想象一下以下简化代码:
#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
存储到堆栈中,并传递地址。
标准中是否有任何内容规定了在这种情况下要做什么(或者严格由编译器决定)?
[dcl.init.ref].
除了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
视为常量值或右值,这可能会导致不同的方法。例如,如果被调用的函数是内联的,编译器可能会生成使用该特定值作为处理器指令常量参数的代码。这里的关键词是"应该是"one_answers"可能",这与一般标准文档中常见的免责条款中的"必须"一样不同。
对于基元类型,对于时态值和constexpr
,您使用的模板签名没有区别。编译器实际如何实现它,取决于平台和编译器。。。以及使用的调用约定。我们甚至不能确定某个东西是否在堆栈中,因为有些平台没有堆栈,或者它的实现与x86平台上的堆栈不同。多种现代调用约定确实使用CPU的寄存器来传递参数。
如果你的编译器足够现代,你根本不需要引用,那么复制省略将使你免于额外的复制操作。证明:
#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
通过引用传递,通过const引用传递不会比通过值传递花费更多,尤其是对于模板。如果您需要不同的语义,您将需要明确的模板专门化。一些较旧的编译器无法以适当的方式支持后者。
template<typename T>
void foo(const T& x) { std::cout << x.baz << std::endl; }
// ...
bar b(42);
foo(b);
输出:
Constructor called
42
如果它是一个左值,例如,那么非常量引用将不允许我们转发参数
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
转发引用是对不合格简历的右值引用模板参数。如果P是转发引用,并且参数为对于类型推导。
您的示例#1。常量位置完全取决于编译器,并且没有在标准中定义。Linux上的GCC可能会在静态只读内存部分中分配这样的常量。优化可能会将其全部删除。
您的示例#2将不会编译(在链接之前)。由于作用域规则。所以你需要bar::baz
。
示例#3,我通常这样做:
template<typename T>
void foo(const T& x) { std::cout << x << std::endl; }