15

I have a structure template that takes two types (T and S), and at some point uses a static_cast to convert from one type to the other. It is often the case that T and S are the same type.

A simplified example of the setup:

template <typename T, typename S = T>
struct foo
{
  void bar(T val)
  {
    /* ... */
    some_other_function(static_cast<S>(val));
    /* ... */
  }
};

In the case that S is the same class as T, does or can the static_cast introduce extra overhead, or is it a null operation which will always be ignored?

If it does introduce overhead, is there a simple template metaprogramming trick to perform the static_cast only if needed, or will I need to create a partial specialization to cope with the T == S case? I'd rather avoid the partial specialization of the entire foo template if possible.

hauzer
  • 258
  • 3
  • 12
marack
  • 2,024
  • 22
  • 31
  • 1
    There won't be any overhead, if they're the same type it will be a no-op (similar to `int a = 5; int b = static_cast(a);`) – Jonathan Potter Oct 01 '13 at 02:10
  • Is this true even for complex types like `std::string`? For example, if `some_other_function`'s argument is `const std::string&`, is it guaranteed to be passed the reference to `val` as opposed to a temporary copy of `val`? – marack Oct 01 '13 at 02:31
  • @JonathanPotter: Unfortunately that is just false. I checked g++ 5.3, g++ 6.1, and clang++ 7.3, and all of them call the copy constructor to build a temporary object for static_cast to the same type. – Nick Matteo May 13 '16 at 18:34
  • @Kundor Well in the above (and below) examples, the copy constructor would come from calling `some_other_function`, `bar` etc. and passing by value. Not from the `static_cast` itself. – Jonathan Potter May 13 '16 at 20:30
  • @JonathanPotter: No. `t2 = static_cast(t1)` calls first a copy constructor (to make an unnamed temporary) and then the assignment operator, when everything is of type `T`. It is not the same as `t2 = t1` (which just calls the assignment operator.) Indeed, the standard specifies that `static_cast(t1)` is identical to declaring `T temp(t1)`, (where `temp` is really an unnamed variable) and the expression has the value `temp`. – Nick Matteo May 13 '16 at 20:41

2 Answers2

13

Yes, it can.

Here is an example:

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";
}

basically, a static_cast can induce a copy constructor to be called.

For some types (like int), a copy constructor is basically free, and the compiler can eliminate it.

For other types, it cannot. In this context, copy elision isn't legal either: if your copy constructor has side effects or the compiler cannot prove that it has no side effects (common if the copy constructor is non-trivial), it will be called.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    It seems to me that copy elision is legal here, since the C++ standard specifically allows to elide any copy to the same type, even if the copy constructor has side effects. Unfortunately the compilers I've checked do not elide the copy for static_cast to the same type. They elide one copy in `T t2 = static_cast(t1);`, so it's the same as `T t2(t1);`. But an assignment `t2 = static_cast(t1)` is the same as `T temp(t1); t2 = temp`. – Nick Matteo May 13 '16 at 18:42
  • @Kundor citation for that? I am aware of cases where elision is legal; I wasn't aware it applied to `static_cast` in particular. Elision is about lifetime, and yes it can eliminate side effects, but that isn't why I thought it wasn't elidable. – Yakk - Adam Nevraumont May 13 '16 at 18:51
  • I was going by the [Wikipedia description](https://en.wikipedia.org/wiki/Copy_elision) "when a temporary object of class type is copied to an object of the same type." But I was wrong, because this is a copy of a named object to a temporary, so it doesn't apply. – Nick Matteo May 13 '16 at 19:21
5

To complement Yakk's answer, I've decided to post some assembly to confirm this. I've used std::string as the test type.

foo<std::string>.bar() - No casting

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


This code is only generated with -O0. Any optimization level whatsoever will even out the two cases.

Community
  • 1
  • 1
hauzer
  • 258
  • 3
  • 12