Может ли 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.


person marack    schedule 01.10.2013    source источник
comment
Никаких накладных расходов не будет, если они одного типа, это будет неработоспособность (аналогично int a = 5; int b = static_cast<int>(a);)   -  person Jonathan Potter    schedule 01.10.2013
comment
Верно ли это даже для сложных типов, таких как std::string? Например, если аргумент some_other_function равен const std::string&, гарантированно ли будет передана ссылка на val, а не на временную копию val?   -  person marack    schedule 01.10.2013
comment
@JonathanPotter: К сожалению, это просто ложь. Я проверил g++ 5.3, g++ 6.1 и clang++ 7.3, и все они вызывают конструктор копирования для создания временного объекта для static_cast того же типа.   -  person Nick Matteo    schedule 13.05.2016
comment
@Kundor Что ж, в приведенных выше (и ниже) примерах конструктор копирования будет исходить из вызова some_other_function, bar и т. д. и передачи по значению. Не от самого static_cast.   -  person Jonathan Potter    schedule 13.05.2016
comment
@JonathanPotter: Нет. t2 = static_cast<T>(t1) сначала вызывает конструктор копирования (чтобы сделать безымянное временное), а затем оператор присваивания, когда все имеет тип T. Это не то же самое, что t2 = t1 (который просто вызывает оператор присваивания). Действительно, стандарт указывает, что static_cast<T>(t1) идентично объявлению T temp(t1) (где temp на самом деле является безымянной переменной), а выражение имеет значение temp.   -  person Nick Matteo    schedule 13.05.2016


Ответы (2)


Да, оно может.

Вот пример:

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

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

person Yakk - Adam Nevraumont    schedule 01.10.2013
comment
Мне кажется, что исключение копирования здесь законно, поскольку стандарт C++ специально разрешает исключать любую копию того же типа, даже если конструктор копирования имеет побочные эффекты. К сожалению, компиляторы, которые я проверял, не исключают копию для static_cast до того же типа. Они пропускают одну копию в T t2 = static_cast<T>(t1);, так что это то же самое, что и T t2(t1);. Но назначение t2 = static_cast<T>(t1) такое же, как T temp(t1); t2 = temp. - person Nick Matteo; 13.05.2016
comment
@Kundor цитата за это? Я знаю случаи, когда элизия является законной; Я не знал, что это применимо к static_cast в частности. Элизион действует на всю жизнь, и да, он может устранить побочные эффекты, но я не поэтому думал, что его нельзя устранить. - person Yakk - Adam Nevraumont; 13.05.2016
comment
Я исходил из описания в Википедии, когда временный объект типа класса копируется в объект типа того же типа. Но я ошибся, потому что это копия именованного объекта во временный, поэтому он не применяется. - person Nick Matteo; 13.05.2016

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

person hauzer    schedule 01.10.2013
comment
В общем, обсуждать производительность, сравнивая сборки, сгенерированные без оптимизаций, бессмысленно. - person Matteo Italia; 01.10.2013
comment
@MatteoItalia Да, это так. Я все еще думал, что было бы интересно сравнить эти два. - person hauzer; 01.10.2013