让函数按非内置类型的 const 值返回的用例有哪些?

发布于 2024-11-14 22:04:49 字数 208 浏览 4 评论 0原文

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

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

我正在努力理解这样做的好处,一旦对象被返回,肯定是调用者的选择决定返回的对象是否应该是 const?

Recently I have read that it makes sense when returning by value from a function to qualify the return type const for non-builtin types, e.g.:

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

I am struggling to understand the benefits of this, once the object has been returned surely it's the callers choice to decide if the returned object should be const?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

瀟灑尐姊 2024-11-21 22:04:49

基本上,这里有一个轻微的语言问题。

std::string func() {
    return "hai";
}

func().push_back('c'); // Perfectly valid, yet non-sensical

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

Basically, there's a slight language problem here.

std::string func() {
    return "hai";
}

func().push_back('c'); // Perfectly valid, yet non-sensical

Returning const rvalues is an attempt to prevent such behaviour. However, in reality, it does way more harm than good, because now that rvalue references are here, you're just going to prevent move semantics, which sucks, and the above behaviour will probably be prevented by the judicious use of rvalue and lvalue *this overloading. Plus, you'd have to be a bit of a moron to do this anyway.

提笔书几行 2024-11-21 22:04:49

偶尔还是有用的。请参见此示例:

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,则编译失败。

It is occasionally useful. See this example:

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;
}

Note that the value returned by operator+ would normally be considered a temporary. But it's clearly being modified. That's not exactly desired.

If you declare the return type of operator+ as const I, this will fail to compile.

浅听莫相离 2024-11-21 22:04:49

按值返回没有任何好处。这没有道理。

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

class Foo
{
    void bar();
};

const Foo foo();

int main()
{
    foo().bar(); // Invalid
}

There is no benefit when returning by value. It doesn't make sense.

The only difference is that it prevents people from using it as an lvalue:

class Foo
{
    void bar();
};

const Foo foo();

int main()
{
    foo().bar(); // Invalid
}
琴流音 2024-11-21 22:04:49

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

它需要以下条件的组合:

  • 您有一个可复制且可移动的类Base
  • 您有一个从 Base 派生的不可复制、不可移动的类 Derived
  • 您真的非常不希望 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<> 指定类型参数,这意味着编译器应该能够从参数的类型中推断出它,因此需要 Base > 实际上是 Derived 的真实基础。

调用 oops_use(Base) 时应发生隐式转换。为此,create_driven() 的结果被具体化为临时 Derived 值,然后将其移至 oops_use 的参数中通过 Base(Base&&) 移动构造函数。因此,物化临时对象现在已移出,并且断言失败。

我们无法删除该移动构造函数,因为它会使 Base 不可移动。我们无法真正阻止 Base&& 绑定到 Derived&& (除非我们显式删除 Base(Derived&&),应对所有派生类执行此操作)。

因此,这里唯一没有 Base 修改的解决方案是使 create_driven() 返回 const Derived,以便 oops_use< /code> 的参数的构造函数无法从物化临时对象中移动。

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

Last year I've discovered another surprising usecase while working on a two-way C++-to-JavaScript bindings.

It requires a combination of following conditions:

  • You have a copyable and movable class Base.
  • You have a non-copyable non-movable class Derived deriving from Base.
  • You really, really do not want an instance of Base inside Derived to be movable as well.
  • You, however, really want slicing to work for whatever reason.
  • All classes are actually templates and you want to use template type deduction, so you cannot really use Derived::operator const Base&() or similar tricks instead of public inheritance.
#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());
}

The fact that I did not specify the type argument for oops_use<> means that the compiler should be able to deduce it from argument's type, hence the requirement that Base<T> is actually a real base of Derived<T>.

An implicit conversion should happen when calling oops_use(Base<T>). For that, create_derived()'s result is materialized into a temporary Derived<T> value, which is then moved into oops_use's argument by Base<T>(Base<T>&&) move constructor. Hence, the materialized temporary is now moved-from, and the assertion fails.

We cannot delete that move constructor, because it will make Base<T> non-movable. And we cannot really prevent Base<T>&& from binding to Derived<T>&& (unless we explicitly delete Base<T>(Derived<T>&&), which should be done for all derived classes).

So, the only resolution without Base modification here is to make create_derived() return const Derived<T>, so that oops_use's argument's constructor cannot move from the materialized temporary.

I like this example because not only it compiles both with and without const without any undefined behaviour, it behaves differently with and without const, and the correct behavior actually happens with const only.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文