c++ - 完美的转发和构造函数

标签 c++ perfect-forwarding temporary-objects

我试图了解完美转发和构造函数的相互作用。我的示例如下:

#include <utility>
#include <iostream>


template<typename A, typename B>
using disable_if_same_or_derived =
  std::enable_if_t<
    !std::is_base_of<
      A,
      std::remove_reference_t<B>
    >::value
  >;


template<class T>
class wrapper {
  public:
    // perfect forwarding ctor in order not to copy or move if unnecessary
    template<
      class T0,
      class = disable_if_same_or_derived<wrapper,T0> // do not use this instead of the copy ctor
    > explicit
    wrapper(T0&& x)
      : x(std::forward<T0>(x))
    {}

  private:
    T x;
};


class trace {
  public:
    trace() {}
    trace(const trace&) { std::cout << "copy ctor\n"; }
    trace& operator=(const trace&) { std::cout << "copy assign\n"; return *this; }
    trace(trace&&) { std::cout << "move ctor\n"; }
    trace& operator=(trace&&) { std::cout << "move assign\n"; return *this; }
};


int main() {
  trace t1;
  wrapper<trace> w_1 {t1}; // prints "copy ctor": OK

  trace t2;
  wrapper<trace> w_2 {std::move(t2)}; // prints "move ctor": OK

  wrapper<trace> w_3 {trace()}; // prints "move ctor": why?
}

我希望自己的wrapper完全没有开销。特别是,当像w_3一样将临时文件编码到包装器中时,我希望可以直接在适当位置创建trace对象,而不必调用move ctor。但是,有一个move ctor调用,这使我认为创建了一个临时对象,然后将其从中移出。为什么叫移动ctor?怎么不称呼它呢?

最佳答案

I would expect the trace object to be created directly in place, without having to call the move ctor.



我不知道你为什么要这样。转发正是这样做的:移动或复制1)。在您的示例中,您使用trace()创建一个临时目录,然后转发将其移至x

如果要就地构造T对象,则需要将参数传递给T的构造,而不是要移动或复制的T对象。

创建一个就位的构造函数:
template <class... Args>
wrapper(std::in_place_t, Args&&... args)
    :x{std::forward<Args>(args)...}
{}

然后这样称呼它:
wrapper<trace> w_3 {std::in_place};
// or if you need to construct an `trace` object with arguments;
wrapper<trace> w_3 {std::in_place, a1, a2, a3};

解决OP对另一个答案的评论:

@bolov Lets forget perfect forwarding for a minute. I think the problem is that I want an object to be constructed at its final destination. Now if it is not in the constructor, it is now garanteed to happen with the garanteed copy/move elision (here move and copy are almost the same). What I don't understand is why this would not be possible in the constructor. My test case proves it does not happen according to the current standard, but I don't think this should be impossible to specify by the standard and implement by compilers. What do I miss that is so special about the ctor?



在这方面,ctor绝对没有什么特别的。您可以使用简单的免费函数看到完全相同的行为:
template <class T>
auto simple_function(T&& a)
{
    X x = std::forward<T>(a);
    //  ^ guaranteed copy or move (depending on what kind of argument is provided
}

auto test()
{
    simple_function(X{});
}

上面的示例与您的OP类似。您可以在simple_function中将x视为类似于包装构造函数,而将本地wrapper变量视为与您的数据成员类似。就这一点而言,机制是相同的。

为了理解为什么不能直接在simple_function的本地范围内构造对象(或在情况下作为包装对象的数据成员),您需要了解在C++ 17中保证复制省略是如何工作的,我建议this excelent answer

总结一下答案:基本上,一个prvalue表达式不会实现一个对象,而是可以初始化一个对象的东西。在将表达式用于初始化对象之前,请保留尽可能长的时间(这样可以避免某些复制/移动)。请参阅链接的答案,以得到更深入但更友好的解释。

使用表达式初始化simple_foo的参数(或构造函数的参数)的那一刻,您被迫具体化一个对象并丢失了表达式。从现在开始,您不再具有原始的prvalue表达式,而是创建了一个物化对象。现在,该对象需要移动到您的最终目标位置-我的本地x(或您的数据成员x)。

如果稍微修改一下示例,我们可以看到有保证的复制省略在起作用:
auto simple_function(X a)
{
    X x = a;
    X x2 = std::move(a);
}


auto test()
{
    simple_function(X{});
}

没有省略,事情会像这样:
  • X{}创建一个临时对象作为simple_function的参数。让我们称之为Temp1
  • Temp1现在已移动(因为它是一个prvalue)到a
  • 的参数simple_function
  • a被复制(因为a是左值)到x
  • a已移动(因为std::movea转换为xvalue)到x2

  • 现在有了C++ 17保证的复制省略
  • X{}不再当场实现对象。而是保留表达式。
  • 现在可以通过a表达式初始化simple_function的参数X{}。无需复制或移动,也不需要。

  • 其余部分现在相同:
  • a复制到x1
  • a已移至x2

  • 您需要了解的内容:命名某事后,某物必须存在。令人惊讶的简单原因是,一旦您为某件事取了个名字,就可以多次引用它。看到我对这个other question的回答。您已将参数命名为wrapper::wrapper。我已将参数命名为simple_function。那是您丢失prvalue表达式以初始化该命名对象的时刻。

    如果您想使用C++ 17保证的复制省略,而又不喜欢就地方法,则需要避免命名:)您可以使用lambda来做到这一点。我最常看到的成语是就地方式,包括在标准中。由于我还没有在野外看到过lambda方式,所以我不知道是否会推荐它。无论如何,这里:
    template<class T> class wrapper {
    public:
    
        template <class F>
        wrapper(F initializer)
            : x{initializer()}
        {}
    
    private:
        T x;
    };
    
    auto test()
    {
        wrapper<X> w = [] { return X{};};
    }
    

    在C++ 17中,此被授予者不进行任何复制和/或移动,并且即使X删除了复制构造函数和move构造函数,它也起作用。就像您想要的那样,该对象将在其最终目的地被构造。

    1)我说的是正确使用的转发习语。 std::forward只是类型转换。

    关于c++ - 完美的转发和构造函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52498654/

    相关文章:

    c++在其自己的类中创建对象的修改拷贝

    c++ - 为什么C++偏向析构函数异常?

    c++ - std::forward<T> 和 std::forward<decltype(t)> 有什么区别?

    c++ - 什么时候不使用 std::forward 和 r 值?

    c++ - 转发可从函数指针类型间接推导的参数

    c++ - 在 GPU 上使用 CUDA 并行化简单算法

    C++ - 专门化类模板的成员函数

    c++ - 临时对象创建

    c++ - 如何正确地从函数返回中 move 对象?

    c++ - 如何允许临时对象使用非常量复制构造函数