【问题标题】:Passing literal as a const ref parameter将文字作为 const ref 参数传递
【发布时间】:2018-01-20 13:32:32
【问题描述】:

想象一下下面的简化代码:

#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内部发生的事情,因为它与这个问题无关。

【问题讨论】:

  • 也许为 T&& 和 T 生成的代码对于 42 是相同的,它只是函数中寄存器中的一个值,而不管它是如何传递给函数的?
  • 如果这个关于编译器实现的问题,真的没有定义,作为 constexpr 值,编译器可以movl 将 42 写入代码中。

标签: c++ c++11 one-definition-rule forwarding-reference pass-by-const-reference


【解决方案1】:

编译器是否将 42 粘贴在某处(在堆栈上?)并将其地址传递给 foo

创建一个const int 类型的临时对象,使用纯右值表达式42 进行初始化,并绑定到引用。

实际上,如果foo 未内联,则需要在堆栈上分配空间,将42 存储到其中并传递地址。

标准中是否有任何规定在这种情况下要做什么(或者它是否严格取决于编译器)?

[dcl.init.ref].

除了 ODR 之外,为什么编译器不能像上面的 42 那样做呢?

因为根据语言,引用绑定到对象bar::baz,除非编译器确切知道foo 在编译调用时正在做什么,否则它必须假设这是重大。例如,如果foo 包含assert(&amp;x == &amp;bar::baz);,则不得使用foo(bar::baz) 触发。

(在 C++17 中,bazimplicitly inline as a constexpr static data member;不需要单独定义。)

有没有一种优雅的方式告诉foo 接受x 的原始类型值?

在没有分析数据表明传递引用实际上会导致问题的情况下,这样做通常没有多大意义,但如果出于某种原因确实需要这样做,添加(可能是 SFINAE 约束的)重载将是要走的路。

【讨论】:

  • “在 C++17 中,baz 作为 constexpr 静态数据成员隐式内联...”您能否添加对标准的引用?如果bar 的类型是std::chrono::milliseconds 怎么办?它还会被内联吗?
  • “在没有分析数据表明传递引用实际上导致问题的情况下,这样做通常没有多大意义......”我在思考一个非常复制成本高。如果我理解正确,在这种情况下必须使用模板重载吗?即使foo 是一个冗长的函数?
  • @InnocentBystander 如果您的模板可以接受昂贵的复制类,请通过引用传递。然后,如果某些使用原始类型的调用实际上导致了性能问题,请添加重载以按值传递那些复制成本低的类型。
  • 很公平。我希望有办法避免这种情况,但我可能只是将其作为一个单独的问题提出
  • 如果有人感兴趣,这里是 P0386R2,它涵盖了 c++17 中的内联变量。
【解决方案2】:

在 C++17 中,考虑到 bar::baz 作为内联使用,代码编译完美,在 C++14 中,模板需要 prvalue 作为参数,因此编译器在目标代码中保留 bar::baz 的符号。这不会得到解决,因为你没有那个声明。 constexpr 应该被编译器视为 constprvalue 或 rvalues,在可能导致不同方法的代码生成中。例如。如果调用的函数是内联的,编译器可能会生成使用该特定值作为处理器指令的常量参数的代码。这里的关键词是“应该”和“可能”,它们与一般标准文档状态中通常的免责声明条款中的“必须”不同。

对于原始类型,时间值和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

转发引用是对不合格 cv 的右值引用 模板参数。如果 P 是转发引用并且参数是 一个左值,类型“对 A 的左值引用”用于代替 A 类型推导。

【讨论】:

  • “使用移动语义实际上很危险” - that isn't move semantics。我认为这个问题比你想象的要复杂。
  • bar::baz 中缺少范围是一个错字。我已经修好了
  • @Swift 好吧,那么您希望注意到您所说的 result 在折叠后不会将这些参数推导出为右值引用,根据标准定义。它也不会改变我最初所说的。没有移动语义,其主题已从您的答案中删除。在指控被凌空抽射之前,不,我不是拒绝这个答案的人。我唯一真正的问题已通过您的编辑得到解决,而且问题很小,以至于我一开始就不会打勾
  • @Swift 因为优化而编译。我会在我的问题中说得更清楚
  • @Swift clang 也支持它,但我同意。它真的很震撼,当您怀疑模板编程时 wtf 是否正在进行时,它非常适合将演绎测试组合在一起。它为我节省了许多令人沮丧的推理推理时间。
【解决方案3】:

您的示例#1。常量位置完全取决于编译器,并且没有在标准中定义。 Linux 上的 GCC 可能会在静态只读内存部分中分配此类常量。优化可能会将其全部删除。

您的 example #2 将无法编译(在链接之前)。由于范围规则。所以你需要bar::baz

示例#3,我通常这样做:

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

【讨论】:

  • bar::baz 中缺少范围是一个错字。我已经修好了。
猜你喜欢
  • 1970-01-01
  • 2021-12-14
  • 1970-01-01
  • 2011-01-31
  • 1970-01-01
  • 1970-01-01
  • 2015-10-02
  • 2011-04-15
  • 1970-01-01
相关资源
最近更新 更多