静态转换到相同类型是否会引入运行时开销?

15
我有一个结构模板,它需要两种类型(T 和 S),并且在某个时刻使用 static_cast 将一种类型转换为另一种类型。往往情况下 T 和 S 是相同的类型。
以下是一个简化的示例设置:
template <typename T, typename S = T>
struct foo
{
  void bar(T val)
  {
    /* ... */
    some_other_function(static_cast<S>(val));
    /* ... */
  }
};

如果ST是同一类,则static_cast是否会引入额外的开销,还是一个空操作将总是被忽略?
如果它确实引入了开销,是否有一个简单的模板元编程技巧只在需要时执行static_cast,或者我需要创建一个部分特化来处理T == S情况?如果可能的话,我宁愿避免整个foo模板的部分特化。

1
如果它们是相同类型,那么就不会有额外的开销,这将成为一个无操作(类似于int a = 5; int b = static_cast<int>(a);)。 - Jonathan Potter
即使是像std::string这样的复杂类型,这也是真的吗?例如,如果some_other_function的参数是const std::string&,它是否保证传递对val的引用,而不是val的临时副本? - marack
很遗憾,那是错误的说法。我检查了 g++ 5.3、g++ 6.1 和 clang++ 7.3,所有这些编译器都会调用复制构造函数来构建一个临时对象,以便进行 static_cast 到相同类型。 - Nick Matteo
@Kundor 在上面(和下面)的例子中,复制构造函数将来自于调用 some_other_functionbar 等并通过值传递。而不是来自于 static_cast 本身。 - Jonathan Potter
@JonathanPotter:不是的。t2 = static_cast<T>(t1) 首先调用一个拷贝构造函数(以创建一个未命名的临时变量),然后再调用赋值运算符,当所有东西都是类型 T 时。这与 t2 = t1 不同(它只调用赋值运算符)。实际上,标准规定 static_cast<T>(t1) 等同于声明 T temp(t1),(其中 temp 实际上是一个未命名变量),并且该表达式的值为 temp - Nick Matteo
2个回答

13
是的,可以。以下是一个例子:
struct A {
  A( A const& ) {
    std::cout << "expensive copy\n";
  }
};

template<typename T>
void noop( T const& ) {}
template <typename T, typename S = T>
void bar(T val)
{
  noop(static_cast<S>(val));
}
template <typename T>
void bar2(T val)
{
  noop(val);
}
int main() {
  std::cout << "start\n";
  A a;
  std::cout << "bar2\n";
  bar2(a); // one expensive copy
  std::cout << "bar\n";
  bar(a); // two expensive copies
  std::cout << "done";
}

基本上,static_cast 可以调用复制构造函数。
对于一些类型(如int),复制构造函数基本上是免费的,编译器可以消除它。
但对于其他类型,则不行。在此情况下,复制省略也不合法:如果您的复制构造函数具有副作用或编译器无法证明它没有副作用(如果复制构造函数是非平凡的,则常见),则会调用它。

1
我认为这里的拷贝省略是合法的,因为C ++标准明确允许省略任何对相同类型的拷贝,即使拷贝构造函数具有副作用也可以。不幸的是,我检查过的编译器没有省略静态转换到相同类型的拷贝。它们在T t2 = static_cast<T>(t1);中省略一个拷贝,因此与T t2(t1);相同。但是赋值t2 = static_cast<T>(t1)T temp(t1); t2 = temp相同。 - Nick Matteo
@Kundor 引用一下吗?我知道省略是合法的情况;我不知道它是否特别适用于 static_cast。省略是关于生命周期的,是的,它可以消除副作用,但这不是我认为它不能被省略的原因。 - Yakk - Adam Nevraumont
我之前参考了维基百科的描述(https://en.wikipedia.org/wiki/Copy_elision):“当将一个类类型的临时对象复制到同一类型的对象时,就会发生复制省略”。但是我错了,因为这是从一个命名对象复制到一个临时对象,所以不适用。 - Nick Matteo

5
为了补充 Yakk's answer,我决定发布一些汇编代码来确认这一点。 我使用std::string作为测试类型。 foo<std::string>.bar() - 无需转换
pushq   %rbp
movq    %rsp, %rbp
subq    $32, %rsp
movq    %rcx, 16(%rbp)
movq    %rdx, 24(%rbp)
movq    24(%rbp), %rax
movq    %rax, %rcx
call    _Z19some_other_functionRKSs
nop
addq    $32, %rsp
popq    %rbp
ret

foo<std::string>.bar() - static_cast<T>()

pushq   %rbp
pushq   %rbx
subq    $56, %rsp
leaq    128(%rsp), %rbp
movq    %rcx, -48(%rbp)
movq    %rdx, -40(%rbp)
movq    -40(%rbp), %rdx
leaq    -96(%rbp), %rax
movq    %rax, %rcx
call    _ZNSsC1ERKSs     // std::string.string()
leaq    -96(%rbp), %rax
movq    %rax, %rcx
call    _Z19some_other_functionRKSs
leaq    -96(%rbp), %rax
movq    %rax, %rcx
call    _ZNSsD1Ev    // std::string.~string()
jmp .L12
movq    %rax, %rbx
leaq    -96(%rbp), %rax
movq    %rax, %rcx
call    _ZNSsD1Ev    // std::string.~string()
movq    %rbx, %rax
movq    %rax, %rcx
call    _Unwind_Resume
nop
.L12:
addq    $56, %rsp
popq    %rbx
popq    %rbp
ret


这段代码只使用-O0生成。任何优化级别都会使这两种情况变得相同。


4
通常情况下,讨论性能问题时,比较未经优化的汇编生成代码是没有意义的。 - Matteo Italia
3
是的,没错。尽管如此,我仍认为将它们进行比较会很有趣。 - hauzer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接