【问题标题】:Passing non-const references to rvalues in C++在 C++ 中将非常量引用传递给右值
【发布时间】:2023-03-16 21:11:01
【问题描述】:

在以下代码行中:

bootrec_reset(File(path, size, off), blksize);

使用原型调用函数:

static void bootrec_reset(File &file, ssize_t blksize);

我收到此错误:

libcpfs/mkfs.cc:99:53: 错误:从“文件”类型的右值初始化“文件&”类型的非常量引用无效

libcpfs/mkfs.cc:30:13: 错误:传递 'void bootrec_reset(File&, ssize_t)' 的参数 1

我知道您不能根据标准将非常量引用 (const &) 传递给右值。但是,MSVC 允许您执行此操作(请参阅this question)。 This question 试图解释原因,但答案毫无意义,因为他使用的是对文字的引用,这是一种极端情况,显然应该被禁止。

在给定的示例中,可以清楚地看到将发生以下事件顺序(就像在 MSVC 中一样):

  1. 将调用File 的构造函数。
  2. Fileblksize 的引用被压入堆栈。
  3. bootrec_reset 使用 file
  4. bootrec_reset返回后,临时的File被销毁。

有必要指出File 引用必须是非常量的,因为它是文件的临时句柄,在该文件上调用非常量方法。此外,我不想将File 的构造函数参数传递给bootrec_reset 以在那里构造,我也看不出有任何理由在调用者中手动构造和销毁File 对象。

所以我的问题是:

  1. C++ 标准以这种方式禁止非常量引用的理由是什么?
  2. 如何强制 GCC 允许此代码?
  3. 即将推出的 C++0x 标准是否会改变这一点,或者新标准给我的东西在这里更合适,例如关于右值引用的所有胡言乱语?

【问题讨论】:

    标签: c++ gcc c++11 rvalue-reference


    【解决方案1】:

    是的,普通函数不能将非常量引用绑定到临时对象——但方法可以——这一事实一直困扰着我。 TTBOMK 的基本原理是这样的(来自this comp.lang.c++.moderated thread):

    假设你有:

     void inc( long &x ) { ++x; }
    
     void test() {
         int y = 0;
         inc( y );
         std::cout << y;
     } 
    

    如果您允许inc()long &amp;x 参数绑定到由y 制作的临时long 副本,那么这段代码显然不会达到您的预期——编译器只会默默地生成代码保持y 不变。显然,这是早期 C++ 时代常见的错误来源。

    如果我设计了 C++,我的偏好是允许非常量引用绑定到临时对象,但禁止在绑定到引用时从左值自动转换为临时对象。但是谁知道呢,这很可能会打开另一种蠕虫的罐头……

    【讨论】:

    • 我不知道在绑定到引用时左值被转换为临时值。如果是这样,这个决定是有道理的。 edit:我刚刚测试过,他们没有?
    • @Matt:规则随着 C++0x 的变化而改变。该规则更改会影响例如传递 std::auto_ptr 作为参数。无论如何,当绑定到对 const 的引用时,并且实际参数不是所需的类型(或派生类),则必须引入临时变量。干杯,
    • @Matt:你到底测试了什么?我的 sn-p 不应该编译,因为 C++ 明确不允许将非常量引用绑定到临时对象(最新的 MSVC++ 和 g++ 遵守这一点);由于++x;,将 ref 更改为 const ref 当然会导致编译失败。
    • void blah(int &amp;); then in caller: long a = 1; blah(a); 在 gcc 4.4.3 上给出错误(所以我希望如此,但您另有说明)。
    • 我应该补充一点,我同意,我希望它与您在最后一段中指出的方式相同。
    【解决方案2】:
    • “C++ 标准以这种方式禁止非常量引用的理由是什么?”

    相反约定的实践经验,这就是事情最初的运作方式。 C++ 在很大程度上是一种进化的语言,而不是一种设计的语言。在很大程度上,仍然存在的规则被证明是有效的(尽管 1998 年的标准化发生了一些重大例外,例如臭名昭​​著的export,委员会发明了而不是标准化现有实践)。

    对于绑定规则,一个人不仅有 C++ 方面的经验,而且对 Fortran 等其他语言也有类似的经验。

    正如@j_random_hacker 在他的回答中指出的那样(正如我所写的那样,得分为 0,表明 SO 中的得分确实不能作为质量衡量标准),最严重的问题与隐式转换和重载有关分辨率。

    • “如何强制 GCC 允许此代码?”

    你不能。

    而不是...

    bootrec_reset(File(path, size, off), blksize);
    

    ...写...

    File f(path, size, off);
    bootrec_reset(f, blksize);
    

    或者定义一个适当的重载bootrec_reset。或者,如果需要“聪明”的代码,原则上您可以编写 bootrec_reset(tempref(File(path, size, off)), blksize);,您只需在其中定义 tempref 以返回其参数引用适当地进行 const 转换。但即使这是一个技术解决方案,也不要这样做。

    • “即将到来的 C++0x 标准是否会改变这一点,或者新标准给我的东西在这里更合适,例如所有关于右值引用的胡言乱语?”

    不,没有任何改变给定代码的东西。

    但是,如果您愿意重写,那么您可以使用例如C++0x 右值引用,或上面显示的 C++98 变通方法。

    干杯,

    【讨论】:

      【解决方案3】:

      即将到来的 C++0x 标准是否会改变这一点,还是新标准给我的东西在这里更合适,例如所有关于右值引用的胡言乱语?

      是的。由于每个名称都是左值,因此将任何表达式视为左值几乎是微不足道的:

      template <typename T>
      T& as_lvalue(T&& x)
      {
          return x;
      }
      
      // ...
      
      bootrec_reset(as_lvalue(File(path, size, off)), blksize);
      

      【讨论】:

        【解决方案4】:
        1. 是一个相当随意的决定——例如,当临时对象是方法调用的主题时,允许对临时对象进行非 const 引用(例如“交换技巧”以释放向量分配的内存,std::vector&lt;type&gt;().swap(some_vector);
        2. 没有给临时命名,我认为你不能。
        3. 据我所知,这条规则也存在于 C++0x 中(用于常规引用),但右值引用特别存在,因此您可以将引用绑定到临时对象 - 因此将 bootrec_reset 更改为采用 File &amp;&amp; 应该会使代码合法。

        【讨论】:

        • 与成员访问案例的比较并不是那么好。成员访问运算符不考虑转换。 const 引用的绑定确实如此。
        • 这是一个正交问题。真正导致意外/令人惊讶的行为的是两者的结合(绑定到从隐式转换获得的临时变量);您可以想象允许对临时对象的引用,但不允许在该上下文中发生任何隐式转换。
        • 但是在绑定非常量引用时允许隐式转换。你不是认真地建议把它拿走,是吗?然后我们又回到了绑定到 const 引用而不是非 const 引用的东西,这就是现在的语言。
        • @Ben Voigt:和 Fabian 一样,我不明白为什么在绑定到引用时去掉隐式转换是一个坏主意——你能详细说明一下吗?在我看来,这是一个完美的解决方案。在那些你故意想要创建一个临时右值并绑定它的情况下,你可以使用template&lt;typename U, typename T&gt; U make(T t) { return t; } 并编写例如int &amp;i_know_i_want_a_copy = make&lt;int&gt;(42.69);。 (即,我们仍然允许绑定到正确类型的 现有 临时对象,具有神奇的生命周期延长,就像我们目前对临时对象的 const refs 所做的那样。)
        • @Ben Voigt:实际上,在我之前的评论中,int &amp;i_know_i_want_a_copy = int(42.69); 就足够了……事后看来,不需要那个花哨的函数模板 :)
        【解决方案5】:

        请注意,将 C++0x 称为“乱码”并不能很好地说明您的编码能力或理解该语言的愿望。

        1) 其实并不是那么随意。允许非常量引用绑定到 r 值会导致代码极其混乱。我最近针对 MSVC 提交了一个与此相关的错误,其中非标准行为导致符合标准的代码无法编译和/或编译时出现异常行为。

        在你的情况下,考虑:

        #include <iostream>
        
        template<typename T>
        void func(T& t)
        {
            int& r = t;
            ++r;
        }
        
        int main(void)
        {
            int i = 4;
            long n = 5;
            const int& r = n;
        
            const int ci = 6;
            const long cn = 7;
        
            //int& r1 = ci;
            //int& r2 = cn;
        
            func(i);
            //func(n);
        
            std::cout << r << std::endl;
        }
        

        您要编译哪些注释行?你想让func(i) 改变它的论点而func(n) 不这样做吗?

        2) 您无法编译该代码。您不想拥有该代码。 MSVC 的未来版本可能会删除非标准扩展并且无法编译该代码。相反,使用局部变量。您始终可以使用一对额外的大括号来控制该局部变量的生命周期,并使其在下一行代码之前被销毁,就像临时变量一样。或 r 值引用。

        {
          File ftemp(path, size, off);
          bootrec_reset(ftemp, blksize);
        }
        

        3) 是的,您可以在这种情况下使用 C++0x 右值引用。

        【讨论】:

        • 在我看来,您的第一个 sn-p 与是否应允许对临时对象进行非常量引用的问题无关:我会说 sn-p 不应单独编译因为 (a) 不应该有从 const 对象到非 const temp rvalue 的自动转换,并且 (b) 应该禁止将非 const ref 带入 const 对象。不知道 (a) 是否实际上已经在 C++ 中,但 (b) 是。
        • @j_random_hacker:我只是在研究一个更复杂的例子。但我不明白你的 (a),r 值没有 constness。你想禁止任何来自 const 对象的自动转换,句号?
        • @Ben:嗯,我的理解有点不稳定,但我认为右值可以有(或没有)常量——尽管差异只对类类型的对象很重要(如果右值是 const 那么只能调用 const 方法)。是的,我建议不应该允许自动转换会从 const 对象产生临时——你是否看到由此产生的意外后果?
        • 我对 (1) 的示例代码感到困惑。 ci、cn、r1 和 r2 的意义何在?你有一个很好的答案,但我还是迷路了。你能把正在发生的事情说得更清楚一点吗?
        • @j_random_hacker:是的,这会破坏很多东西。特别是,我很高兴 C++ 允许这样做:const int i = 5; void func(const long&amp; a); func(i); 现在临时的long 是通过转换i 创建的,即const。而且我不相信右值上的const 修饰符存在。能举个例子吗?
        【解决方案6】:

        或者,简单地重载。

        static void bootrec_reset(File &&file, ssize_t blksize) {
            return bootrec_reset(file, blksize);
        }
        

        这是最简单的解决方案。

        【讨论】:

          【解决方案7】:

          如何强制 GCC 允许此代码?

          如果您拥有 File 的定义,那么您可以尝试玩这样的技巧:

          class File /* ... */ {
          public:
            File* operator&() { return this; }
          /* ... */
          };
          /* ... */
          bootrec_reset(*&File(path, size, off), blksize);
          

          这在 c++98 模式下为我编译。

          即将到来的 C++0x 标准是否会改变这一点,还是新标准给我的东西在这里更合适,例如所有关于右值引用的胡言乱语?

          如果可能的话,显然这是要走的路。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-08-01
            • 2014-09-09
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-03-03
            • 1970-01-01
            相关资源
            最近更新 更多