Представьте себе следующий упрощенный код:
#include <iostream>
void foo(const int& x) { do_something_with(x); }
int main() { foo(42); return 0; }
(1) Помимо оптимизации, что происходит, когда 42 передается foo
?
Компилятор вставляет 42 где-нибудь (в стеке?) И передает свой адрес foo
?
(1a) Есть ли в стандарте что-то, что предписывает, что делать в этой ситуации (или это строго зависит от компилятора)?
Теперь представьте немного другой код:
#include <iostream>
void foo(const int& x) { do_something_with(x); }
struct bar { static constexpr int baz = 42; };
int main() { foo(bar::baz); return 0; }
Это не будет связывать, если я не определю int bar::baz;
(из-за ODR?).
(2) Кроме ODR, почему компилятор не может делать то, что он делал с 42 выше?
Очевидный способ упростить вещи — это определить foo
как:
void foo(int x) { do_something_with(x); }
Но что делать в случае с шаблоном? Например:
template<typename T>
void foo(T&& x) { do_something_with(std::forward<T>(x)); }
(3) Есть ли элегантный способ сказать foo
принять x
по значению для примитивных типов? Или мне нужно специализировать это с SFINAE или что-то подобное?
РЕДАКТИРОВАТЬ: Изменено, что происходит внутри foo
как это не имеет отношения к этому вопросу.
Компилятор вставляет 42 где-нибудь (в стеке?) И передает свой адрес
foo
?
Временный объект типа const int
создается, инициализируется выражением prvalue 42
и привязаны к ссылке.
На практике, если foo
не встраивается, что требует выделения места в стеке, хранения 42
в него и передавая адрес.
Есть ли что-то в стандарте, что диктует, что делать в этой ситуации (или это строго зависит от компилятора)?
Помимо ODR, почему компилятор не может делать то, что он делал с 42 выше?
Потому что в зависимости от языка, ссылка связана с объектом bar::baz
и если компилятор точно не знает, что foo
делает в точке, где он компилирует вызов, то он должен предположить, что это важно. Например, если foo
содержит assert(&x == &bar::baz);
, тот не должен огонь с foo(bar::baz)
,
(В C ++ 17 baz
является неявно встроенный как constexpr
член статических данных; отдельного определения не требуется.)
Есть ли элегантный способ сказать
foo
принятьx
по значению для примитивных типов?
Обычно нет особого смысла делать это в отсутствие данных профилирования, показывающих, что передача по ссылке на самом деле вызывает проблемы, но если вам действительно нужно сделать это по какой-то причине, добавление (возможно, SFINAE-ограниченное) перегрузок будет путь
В C ++ 17 этот код прекрасно компилируется с учетом использования bar :: baz как встроенного, в C ++ 14 шаблон требует prvalue в качестве аргумента, поэтому компилятор сохраняет символ для bar::baz
в объектном коде. Который не будет решен, потому что у вас не было этой декларации. constexpr
должен обрабатываться компилятором как constprvalue или rvalues, при генерации кода это может привести к другому подходу. Например. если вызываемая функция встроенная, компилятор может генерировать код, который использует это конкретное значение в качестве постоянного аргумента инструкции процессора. Ключевыми словами здесь являются слова «должен быть» и «может», которые отличаются от слова «должен», как и обычное положение об отказе от ответственности в общих состояниях стандартной документации.
Для примитивного типа, для временного значения и constexpr
не будет никакой разницы, в какой подписи шаблона вы используете. То, как на самом деле это реализует компилятор, зависит от платформы и компилятора … и используемых соглашений о вызовах. мы даже не можем точно сказать, находится ли что-то в стеке наверняка, потому что на какой-то платформе нет стека или он реализован иначе, чем стек на платформе x86. Многочисленные современные соглашения о вызовах используют регистры CPU для передачи аргументов.
Если ваш компилятор достаточно современный, вам вообще не нужны ссылки, копирование elision избавит вас от лишних операций копирования. Чтобы доказать это:
#include <iostream>
template<typename T>
void foo(T x) { std::cout << x.baz << std::endl; }#include <iostream>
using namespace std;
struct bar
{
int baz;
bar(const int b = 0): baz(b)
{
cout << "Constructor called" << endl;
}
bar(const bar &b): baz(b.baz) //copy constructor
{
cout << "Copy constructor called" << endl;
}
};
int main()
{
foo(bar(42));
}
приведет к выводу:
Constructor called
42
Передача по ссылке, по константной ссылке не будет стоить дороже, чем передача по значению, особенно для шаблонов. Если вам нужна другая семантика, вам потребуется явная специализация шаблона. Некоторые старые компиляторы не могли должным образом поддерживать последний.
template<typename T>
void foo(const T& x) { std::cout << x.baz << std::endl; }
// ...
bar b(42);
foo(b);
Выход:
Constructor called
42
Неконстантная ссылка не позволила бы нам переадресовать аргумент, если бы он был lvalue, например
template<typename T>
void foo(T& x) { std::cout << x.baz << std::endl; }
// ...
foo(bar(42));
позвонив по этому шаблону (называется идеальной пересылкой)
template<typename T>
void foo(T&& x) { std::cout << x << std::endl; }
можно было бы избежать проблем с пересылкой, хотя этот процесс
также включают в себя копию разрешения. Компилятор выводит параметр шаблона следующим образом из C ++ 17
template <class T> int f(T&& heisenreference); template <class T> int g(const T&&); int i; int n1 = f(i); // calls f<int&>(int&) int n2 = f(0); // calls f<int>(int&&) int n3 = g(i); // error: would call g<int>(const int&&), which // would bind an rvalue reference to an lvalue
Ссылка для пересылки является основной ссылкой на cv-unqualified
параметр шаблона. Если P является ссылкой для пересылки, а аргумент
lvalue, тип «lvalue ссылка на A» используется вместо A для
тип вычета.
Ваш пример №1. Постоянное расположение полностью зависит от компилятора и не определяется стандартом. GCC в Linux может размещать такие константы в статической секции только для чтения. Оптимизация, вероятно, удалит все это вместе.
Ваш пример № 2 не скомпилируется (перед ссылкой). Из-за ограниченных правил. Так вам нужно bar::baz
там.
пример № 3, я обычно делаю это:
template<typename T>
void foo(const T& x) { std::cout << x << std::endl; }