Передача литерала в качестве параметра const ref

Представьте себе следующий упрощенный код:

#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 или некоторыми такими?

EDIT: Изменено то, что происходит внутри foo, поскольку оно не имеет отношения к этому вопросу.

Ответы

Ответ 1

Содержит ли компилятор 42 где-нибудь (в стеке?) и передает его адрес foo?

Создается временный объект типа const int, инициализированный выражением prvalue 42 и привязанный к ссылке.

На практике, если foo не является строчным, это требует выделения пространства в стеке, хранения 42 в нем и передачи адреса.

Есть ли что-нибудь в стандарте, которое диктует, что должно быть сделано в этой ситуации (или это строго до компилятора)?

[dcl.init.ref].

Помимо ODR, почему компилятор не может сделать то, что он сделал с 42 выше?

Поскольку в соответствии с языком ссылка привязана к объекту bar::baz, и если компилятор точно не знает, что делает foo в точке, где он компилирует вызов, тогда он должен предположить, что это значительное. Например, если foo содержит assert(&x == &bar::baz);, который не должен запускаться с помощью foo(bar::baz).

(В С++ 17 baz неявно встроен как статический элемент данных constexpr, не требуется отдельное определение.)

Есть ли элегантный способ сказать foo принять x по значению для примитивных типов?

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

Ответ 2

С С++ 17 этот код отлично компилируется с учетом использования bar:: baz как inline, а для С++ 14 для шаблона требуется prvalue в качестве аргумента, поэтому компилятор сохраняет символ для bar::baz в объектном коде. Которые не будут решены, потому что у вас не было этого заявления. constexpr следует рассматривать как constprvalue или rvalues ​​компилятором при генерации кода, что может привести к разному подходу. Например. если вызываемая функция является встроенной, компилятор может генерировать код, который использует это конкретное значение как постоянный аргумент инструкции процессора. Ключевые слова здесь "должны быть" и "могут", которые отличаются от "must", как обычно, положения о отказе от ответственности в общих стандартах.

Для примитивного типа, для временного значения и constexpr не будет никакой разницы, в которой вы используете подпись шаблона. Как на самом деле компилятор реализует его, зависит от платформы и компилятора... и используемых вызовов. мы даже не можем сказать, действительно ли что-то в стеке, потому что на какой-то платформе нет стека или она реализована иначе, чем стек на платформе x86. Несколько современных соглашений о вызовах используют регистры процессора для передачи аргументов.

Если ваш компилятор достаточно современен, вам вообще не нужны ссылки, копия 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; }

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

Ссылка на пересылку - это ссылка rvalue на cv-unqualified параметр шаблона. Если P является ссылкой пересылки, и аргумент lvalue, тип "lvalue reference to A" используется вместо A для типа.

Ответ 3

Ваш пример # 1. Постоянное местоположение полностью зависит от компилятора и не определено в стандарте. GCC в Linux может выделять такие константы в статичной памяти только для чтения. Оптимизация, вероятно, удалит все вместе.

Ваш пример # 2 не будет компилироваться (до ссылки). Из-за правил съемки. Поэтому вам нужно bar::baz.

пример # 3, Я обычно делаю это:

template<typename T>
    void foo(const T& x) { std::cout << x << std::endl; }