为什么我不能使用 std::tuple 保证复制省略?

Why do I not get guaranteed copy elision with std::tuple?

我希望在 C++20 中,以下代码在 A 和 B 的打印之间不打印任何内容(因为我希望保证 RVO 启动)。但输出是:

A

Bye

B

C

Bye

Bye

所以大概正在创建一个临时文件。

#include <iostream>
#include <tuple>
struct INeedElision{
    int i;
    ~INeedElision(){
        std::cout << "Bye\n";
    }
};

std::tuple<int, INeedElision> f(){
    int i = 47;
    return {i, {47}};
}

INeedElision g(){
    return {};
}

int main()
{   
    std::cout << "A\n"; 
    auto x = f();
    std::cout << "B\n";
    auto y = g();
    std::cout << "C\n";
}

这种行为的原因是什么? 是否有避免复制的解决方法(不使用指针)?

https://godbolt.org/z/zasoGd

{i, {47}} 构造 std::tuple<int, INeedElision> 时,the selected constructor of std::tuple 从 lvalue-reference 到 const 获取元素。

tuple( const Types&... args );

然后当使用 {i, {47}} 作为初始化器时,一个临时的 INeedElision 将被构造,然后传递给 std::tuple 的构造函数(并被复制)。临时对象将立即销毁,您将在“A”和“B”之间看到“再见”。

顺便说一句:std::tuple 的第三个构造函数不会用于这种情况。

template< class... UTypes >
tuple( UTypes&&... args );

它是一个构造器模板,braced-init-list和{47}一样没有类型,不能通过模板参数推导来推导。

另一方面,如果 INeedElision 有一个采用 int 的转换构造函数,并将初始化器设为 {i, 47},则将使用 std::tuple 的第三个构造函数并且没有构造临时 INeedElision ;该元素将从 int 47.

构造 in-place

LIVE

如果你 return 对象本身,你只会得到复制省略:

std::vector<int> fn1()
{
   return std::vector<int>{}; // guaranteed copy elision
}

std::vector<int> fn2()
{
   std::vector<int> vec;
   return vec; // a good compiler will manage to elide the copy/move here
}

在您的情况下,您正在 returning 元组,因此元组本身可能会被复制省略,但传递给元组构造函数的参数不会被省略!

std::tuple<int, INeedElision> f(){

    int i = 47;
    return {i, {47}}; // construct the tuple in place of the return address but the arguments are copied into the tuple and not even moved ! to move call std::move explicitly
}

不允许编译器省略传递给元组构造函数的参数副本,因为您不是return参数本身,而是包含参数副本的元组。另请注意,table 无法保存对参数的引用,因为这些局部变量将在函数 returns 导致悬空引用时被破坏。

如果你想在 C++ 17 中获得复制省略的机会,然后做这样的事情:

std::tuple<int, INeedElision> f(){

    std::tuple<int, INeedElision> ret;
    auto& [i, ne] = ret;
    i = 47;
    ne = 47;
    return ret;
}