【问题标题】:Is it possible to take a parameter by const reference, while banning conversions so that temporaries aren't passed instead?是否可以通过 const 引用获取参数,同时禁止转换以便不传递临时变量?
【发布时间】:2012-01-25 15:02:25
【问题描述】:

有时我们喜欢通过引用来获取一个大参数,并且尽可能将引用设为 const 以宣传它是一个输入参数。但是通过将引用设为 const,编译器随后允许自己转换类型错误的数据。这意味着它的效率不高,但更令人担忧的是我认为我指的是原始数据;也许我会拿它的地址,而没有意识到我实际上是在取一个临时地址。

此代码中对bar 的调用失败。这是可取的,因为引用的类型不正确。对bar_const 的调用也属于错误类型,但它会静默编译。这对我来说是不可取的。

#include<vector>
using namespace std;

int vi;

void foo(int &) { }
void bar(long &) { }
void bar_const(const long &) { }

int main() {
   foo(vi);
   // bar(vi); // compiler error, as expected/desired
   bar_const(vi);
}

传递轻量级只读引用的最安全方法是什么?我很想创建一个类似引用的新模板。

(很明显,intlong 是非常小的类型。但是我已经发现了可以相互转换的较大结构。我不希望这种情况在我采取行动时悄悄发生const 引用。有时,将构造函数标记为显式会有所帮助,但这并不理想)

更新:我想象一个系统如下:想象有两个函数 X byVal();X&amp; byRef(); 以及以下代码块:

 X x;
 const_lvalue_ref<X> a = x; // I want this to compile
 const_lvalue_ref<X> b = byVal(); // I want this to fail at compile time
 const_lvalue_ref<X> c = byRef(); // I want this to compile

该示例基于局部变量,但我希望它也可以使用参数。如果我不小心传递了 ref-to-temporary 或 ref-to-a-copy,而我想我会传递一些轻量级的东西,例如 ref-to-lvalue,我想得到某种错误消息。这只是一个“编码标准”——如果我真的想允许将 ref 传递给临时对象,那么我将使用简单的const X&amp;。 (我发现this piece on Boost's FOREACH 非常有用。)

【问题讨论】:

  • 您说将构造函数标记为显式“有帮助但并不理想”——理想的解决方案是什么?考虑一下 James Kanze 的回答。
  • explicit 构造函数有其用途。例如,当按值传递事物时,或者使用 A a; B b = a; 之类的事物时。在特定情况下,将T&amp; 更改为const T&amp; 不仅仅是禁止修改。将const 添加到引用中并不仅仅是禁止修改似乎有点不一致。 (我迟早会评论所有答案,它们都很有趣。)

标签: c++ constants


【解决方案1】:

好吧,如果你的“大参数”是一个类,首先要做的是确保你将任何单参数构造函数标记为显式(除了复制构造函数):

class BigType
{
public:
    explicit BigType(int);
};

这适用于具有默认参数的构造函数,也可以使用单个参数调用。

然后它不会被自动转换为,因为编译器没有隐式构造函数来进行转换。你可能没有任何全局转换运算符来生成该类型,但如果你有,那么

如果这对您不起作用,您可以使用一些模板魔法,例如:

template <typename T>
void func(const T &); // causes an undefined reference at link time.

template <>
void func(const BigType &v)
{
    // use v.
}

【讨论】:

  • 我敢打赌,您可以将依赖于类型的static_assert 放入模板函数中,以便在编译时捕获它。
  • 看起来不错。我对这个答案做了进一步的实验。看来定义的函数不需要是模板。即模板函数将吸引所有无效调用,非模板函数可以提供有效的实现。这有意义吗?
  • @Aaron McDaid 您应该(几乎)永远不要将重载与专业化混为一谈,原因是我现在无法理解,但这与编译器并不总能找到您想要的功能有关。
  • 请注意,single parameter constructors 并不完全准确,因为可以使用单个参数调用具有四个默认值的五参数构造函数,实际上它也是一个转换构造函数,也应该标记为显式。跨度>
【解决方案2】:

如果您可以使用 C++11(或其部分),这很容易:

void f(BigObject const& bo){
  // ...
}

void f(BigObject&&) = delete; // or just undefined

Live example on Ideone.

这会起作用,因为绑定到右值 ref 优于绑定到临时对象的引用到常量。

您还可以利用隐式转换序列中只允许单个用户定义转换的事实:

struct BigObjWrapper{
  BigObjWrapper(BigObject const& o)
    : object(o) {}

  BigObject const& object;
};

void f(BigObjWrapper wrap){
  BigObject const& bo = wrap.object;
  // ...
}

Live example on Ideone.

【讨论】:

    【解决方案3】:

    这很容易解决:停止通过引用获取值。如果要确保参数是可寻址,则将其设为地址

    void bar_const(const long *) { }
    

    这样,用户必须传递一个指针。而且您无法获得指向临时对象的指针(除非用户是非常恶意的)。

    话虽如此,我认为你对这件事的想法是……错误的。到此为止。

    也许我会拿它的地址,但没有意识到我实际上是在取一个临时地址。

    获取恰好是临时地址的const&amp; 的地址实际上没问题。问题是您不能长期存储它。您也不能转让它的所有权。毕竟,您有一个 const 参考。

    这是问题的一部分。如果您使用const&amp;,您的界面会显示,“我可以使用这个对象,但我不拥有它,也不能将所有权转让给其他人。”由于您不拥有该对象,因此您无法长期存储它。这就是const&amp; 的意思。

    改用const* 可能会有问题。为什么?因为你不知道那个指针是从哪里来的。谁拥有这个指针? const&amp; 有许多句法保护措施来防止你做坏事(只要你不拿它的地址)。 const* 什么都没有;您可以将该指针复制到您心中的内容。您的界面没有说明您是否可以拥有该对象或将所有权转让给其他人。

    这种歧义是 C++11 具有像 unique_ptrshared_ptr 这样的智能指针的原因。这些指针可以描述真实的内存所有权关系。

    如果您的函数按值获取unique_ptr,那么您现在拥有该对象。如果它需要shared_ptr,那么您现在共享该对象的所有权。有句法保证来确保所有权(同样,除非您采取不愉快的步骤)。

    如果你不使用 C++11,你应该使用 Boost 智能指针来实现类似的效果。

    【讨论】:

      【解决方案4】:

      你不能,即使你能,也可能不会有太大帮助。 考虑:

      void another(long const& l)
      {
          bar_const(l);
      }
      

      即使您可以以某种方式阻止绑定到临时作为输入 bar_const, 像 another 这样的函数可以用引用来调用 绑定到一个临时的,你最终会陷入同样的​​境地。

      如果您不能接受临时文件,则需要使用对 非常量,或指针:

      void bar_const(long const* l);
      

      需要一个左值来初始化它。当然,像

      这样的函数
      void another(long const& l)
      {
          bar_const(&l);
      }
      

      仍然会导致问题。但是,如果您在全球范围内采用约定 如果对象生存期必须超出调用结束,则使用指针, 那么希望another的作者会思考他为什么要服用 地址,并避免它。

      【讨论】:

      • 我想我想要一个约定,&amp; 仅适用于非常量参数。这样我就可以阅读bar(a,b,&amp;c) 并看到ab 是输入,c 将存储输出。您的回答暗示了相反的约定。
      • @AaronMcDaid 有许多不同的可能约定。基本上,我们有两种选择,指针和引用,但值得做出的区别不止两个。我见过的约定包括尽可能使用引用,如果对象的生命周期必须超出函数调用,则使用指针,如果对象将被修改,则使用指针;我相信其他人也是合理的。重要的是项目中的每个人都使用相同的约定。
      【解决方案5】:

      我认为您使用 intlong 的示例有点像在规范 C++ 中的红鲱鱼,无论如何您永远不会通过 const 引用传递内置类型:您通过值或非常量引用传递它们。

      因此,让我们假设您有一个大型用户定义类。在这种情况下,如果它为您创建临时对象,那么这意味着您为该类创建了隐式转换。您所要做的就是将所有转换构造函数(可以使用单个参数调用的构造函数)标记为explicit,编译器将阻止自动创建这些临时构造函数。例如:

      class Foo
      {
          explicit Foo(int bar) { }
      };
      

      【讨论】:

        【解决方案6】:

        (感谢this great answer 回答我自己的问题。感谢@hvd。)

        简而言之,将函数参数标记为volatile 意味着它不能绑定到右值。 (任何人都可以为此确定一个标准报价吗?临时可以绑定到const&amp;,但显然不能绑定到const volatile &amp;。这就是我在g++-4.6.1上得到的。(额外:见this extended comment stream有些血腥方式在我头上的细节:-)))

        void foo( const volatile Input & input, Output & output) {
        }
        
        foo(input, output); // compiles. good
        foo(get_input_as_value(), output); // compile failure, as desired.
        

        但是,您实际上不希望参数为volatile。所以我写了一个小包装器来 const_cast volatile 。所以 foo 的签名变成了这样:

        void foo( const_lvalue<Input> input, Output & output) {
        }
        

        包装器在哪里:

        template<typename T>
        struct const_lvalue {
            const T * t;
            const_lvalue(const volatile T & t_) : t(const_cast<const T*>(&t_)) {}
            const T* operator-> () const { return t; }
        };
        

        这只能从左值创建

        有什么缺点吗?这可能意味着我不小心误用了一个真正易变的对象,但是我之前从未使用过volatile。所以我认为这对我来说是正确的解决方案。

        我希望养成在默认情况下使用所有合适参数的习惯。

        Demo on ideone

        【讨论】:

          猜你喜欢
          • 2018-10-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-02-10
          相关资源
          最近更新 更多