【问题标题】:What are the use cases for having a function return by const value for non-builtin type?非内置类型的 const 值返回函数的用例是什么?
【发布时间】:2011-06-09 22:15:14
【问题描述】:

最近我读到,从函数中按值返回来限定非内置类型的返回类型 const 是有意义的,例如:

const Result operation() {
    //..do something..
    return Result(..);
}

我很难理解这样做的好处,一旦对象返回肯定是调用者的选择来决定返回的对象是否应该是 const?

【问题讨论】:

    标签: c++ const-correctness


    【解决方案1】:

    基本上,这里有一点语言问题。

    std::string func() {
        return "hai";
    }
    
    func().push_back('c'); // Perfectly valid, yet non-sensical
    

    返回 const rvalues 是为了防止这种行为。然而,实际上,它弊大于利,因为现在有了右值引用,你只会阻止移动语义,这很糟糕,并且上述行为可能会通过明智地使用右值和左值来防止*this 超载。另外,无论如何,你必须有点白痴才能做到这一点。

    【讨论】:

    • 您确定将右值标记为 const 会阻止移动语义吗?我的 clang 版本允许使用 const 右值调用自定义移动构造函数(尽管它不会让非 const 右值引用绑定到它,所以我不完全确定发生了什么)。
    • @John Calsbeek:你怎么能从一个 const rvalue 移动?你不能修改它——因为它是常量。
    • @John:但是你的 const move 构造函数真的可以执行移动吗?例如。假设您的类有一个需要释放的原始指针。在移动构造函数中,您复制了指针,但您还必须将 RHS 对象中的副本设置为 NULL,这样它就不会被双重删除。但是如果是const rvalue,就不能修改了……
    • void 方法是荒谬的。对于返回某些东西的方法,它非常有意义。例如,如果string::push_back 返回了对字符串本身的引用,您就可以使用string s = func().push_back('c');(例如)。
    • 我设法让我的 clang 版本通过移动构造函数将const T 绑定到T&&,并通过右值引用对其进行变异。我只是想弄清楚这是否是clang中的错误。 (我想不出一个“真正的 const”右值,除了一个从声明为 const 的左值转换而来的右值。)
    【解决方案2】:

    它偶尔有用。看这个例子:

    class I
    {
    public:
        I(int i)                   : value(i) {}
        void set(int i)            { value = i; }
        I operator+(const I& rhs)  { return I(value + rhs.value); }
        I& operator=(const I& rhs) { value = rhs.value; return *this; }
    
    private:
        int value;
    };
    
    int main()
    {
        I a(2), b(3);
        (a + b) = 2; // ???
        return 0;
    }
    

    请注意,operator+ 返回的值通常被视为临时值。但它显然正在被修改。这并不完全是我们想要的。

    如果将operator+的返回类型声明为const I,则编译失败。

    【讨论】:

    • “Effective C++”中的第 3 项
    【解决方案3】:

    按值返回没有任何好处。这没有意义。

    唯一的区别是它阻止人们将它用作左值:

    class Foo
    {
        void bar();
    };
    
    const Foo foo();
    
    int main()
    {
        foo().bar(); // Invalid
    }
    

    【讨论】:

    • 这实际上是不正确的。它根本不会阻止人们将它用作左值。它阻止人们将其用作非常量对象。这是两个完全不同的东西。
    【解决方案4】:

    去年我在处理双向 C++ 到 JavaScript 绑定时发现了另一个令人惊讶的用例。

    它需要以下条件的组合:

    • 你有一个可复制和可移动的类Base
    • 您有一个不可复制的不可移动类 Derived 派生自 Base
    • 您真的,真的不希望Derived 中的Base 实例也可以移动。
    • 但是,无论出于何种原因,您都希望切片能够正常工作。
    • 所有的类实际上都是模板,你想使用模板类型推导,所以你不能真正使用Derived::operator const Base&()或类似的技巧来代替公共继承。
    #include <cassert>
    #include <iostream>
    #include <string>
    #include <utility>
    
    // Simple class which can be copied and moved.
    template<typename T>
    struct Base {
        std::string data;
    };
    
    template<typename T>
    struct Derived : Base<T> {
        // Complex class which derives from Base<T> so that type deduction works
        // in function calls below. This class also wants to be non-copyable
        // and non-movable, so we disable copy and move.
        Derived() : Base<T>{"Hello World"} {}
        ~Derived() {
            // As no move is permitted, `data` should be left untouched, right?
            assert(this->data == "Hello World");
        }
        Derived(const Derived&) = delete;
        Derived(Derived&&) = delete;
        Derived& operator=(const Derived&) = delete;
        Derived& operator=(Derived&&) = delete;
    };
    
    // assertion fails when the `const` below is commented, wow!
    /*const*/ auto create_derived() { return Derived<int>{}; }
    
    // Next two functions hold reference to Base<T>/Derived<T>, so there
    // are definitely no copies or moves when they get `create_derived()`
    // as a parameter. Temporary materializations only.
    template<typename T>
    void good_use_1(const Base<T> &) { std::cout << "good_use_1 runs" << std::endl; }
    
    template<typename T>
    void good_use_2(const Derived<T> &) { std::cout << "good_use_2 runs" << std::endl; }
    
    // This function actually takes ownership of its argument. If the argument
    // was a temporary Derived<T>(), move-slicing happens: Base<T>(Base<T>&&) is invoked,
    // modifying Derived<T>::data.
    template<typename T>
    void oops_use(Base<T>) { std::cout << "bad_use runs" << std::endl; }
    
    int main() {
        good_use_1(create_derived());
        good_use_2(create_derived());
        oops_use(create_derived());
    }
    

    我没有为oops_use&lt;&gt; 指定类型参数的事实意味着编译器应该能够从参数的类型中推断出它,因此要求Base&lt;T&gt; 实际上是Derived&lt;T&gt; 的真实基础。

    调用oops_use(Base&lt;T&gt;) 时应该进行隐式转换。为此,create_derived() 的结果被具体化为一个临时的Derived&lt;T&gt; 值,然后通过Base&lt;T&gt;(Base&lt;T&gt;&amp;&amp;) 移动构造函数将其移动到oops_use 的参数中。因此,物化的临时对象现在被移出,断言失败。

    我们不能删除那个移动构造函数,因为它会使Base&lt;T&gt; 不可移动。而且我们不能真正阻止Base&lt;T&gt;&amp;&amp; 绑定到Derived&lt;T&gt;&amp;&amp;(除非我们明确删除Base&lt;T&gt;(Derived&lt;T&gt;&amp;&amp;),这应该对所有派生类执行)。

    所以,这里唯一没有修改Base的解决办法是让create_derived()返回const Derived&lt;T&gt;,这样oops_use的参数的构造函数就不能从物化的临时对象中移动。

    我喜欢这个例子,因为它不仅在有和没有const 的情况下都可以编译而没有任何未定义的行为,而且在有和没有const 的情况下它的行为也不同,而且正确的行为实际上只发生在const 的情况下。

    【讨论】:

    • 当然,最初的一组要求违反了里氏替换原则。
    • 另外,一个更健壮的方法是修改Base,使其不能通过添加模板化的 SFINAEd 复制构造函数从不可移动的派生类中移动。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多