Может ли static_cast использовать один и тот же тип для выполнения служебных задач во время выполнения?

У меня есть шаблон структуры, который принимает два типа (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));
    /* ... */
  }
};

В случае, когда S является тем же классом, что и T, имеет или может static_cast вводить дополнительные служебные данные, или это нулевая операция, которая всегда будет игнорироваться?

Если он вводит накладные расходы, есть ли простой трюк метапрограммирования шаблона, чтобы выполнить static_cast только при необходимости, или мне нужно создать частичную специализацию, чтобы справиться с тегом T == S? Я предпочел бы избежать частичной специализации всего шаблона foo, если это возможно.

Ответ 1

Да, это возможно.

Вот пример:

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) конструктор копирования в основном свободен, и компилятор может его устранить.

Для других типов он не может. В этом контексте исключение для копирования также не является законным: если у вашего конструктора копии есть побочные эффекты или компилятор не может доказать, что он не имеет побочных эффектов (общий, если конструктор копирования нетривиален), он будет вызываться.

Ответ 2

Чтобы дополнить ответ Якка, я решил опубликовать сборку, чтобы подтвердить это. Я использовал 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. Любой уровень оптимизации будет даже устранять два случая.