class 构造函数优先级,带有值包装器的可变模板构造函数

class constructor precedence with a variadic template constructor for a value wrapper

今天发现我不懂C++的构造函数优先级规则

请参阅以下模板struct wrapper

template <typename T>
struct wrapper
 {
   T value;

   wrapper (T const & v0) : value{v0}
    { std::cout << "value copy constructor" << std::endl; }

   wrapper (T && v0) : value{std::move(v0)}
    { std::cout << "value move constructor" << std::endl; }

   template <typename ... As>
   wrapper (As && ... as) : value(std::forward<As>(as)...)
    { std::cout << "emplace constructor" << std::endl; }

   wrapper (wrapper const & w0) : value{w0.value}
    { std::cout << "copy constructor" << std::endl; }

   wrapper (wrapper && w0) : value{std::move(w0.value)}
    { std::cout << "move constructor" << std::endl; }
 };

这是一个简单的模板值包装器,带有复制构造函数 (wrapper const &)、一个移动构造函数 (wrapper && w0)、一种值复制构造函数 (T const & v0)、一种移动构造函数(T && v0)和一种模板就地构造值构造函数(As && ... as,以 STL 容器的 emplace 方法为例)。

我的意图是使用通过包装器调用的复制或移动构造函数,传递 T 对象的值复制或移动构造函数和调用能够构造对象的值列表的模板 emplace 构造函数类型 T.

但是我没有得到我期望的。

来自以下代码

std::string s0 {"a"};

wrapper<std::string> w0{s0};            // emplace constructor (?)
wrapper<std::string> w1{std::move(s0)}; // value move constructor
wrapper<std::string> w2{1u, 'b'};       // emplace constructor
//wrapper<std::string> w3{w0};          // compilation error (?)
wrapper<std::string> w4{std::move(w0)}; // move constructor

w1w2w4 值按预期使用值移动构造函数、放置构造函数和移动构造函数(分别)构造。

但是 w0 是用 emplace 构造函数构造的(我期待值复制构造函数)并且 w3 根本没有构造(编译错误)因为 emplace 构造函数是首选但不是接受 wrapper<std::string> 值的 std::string 构造函数。

第一个问题:我做错了什么?

我想 w0 问题是因为 s0 不是 const 值,所以 T const & 不是完全匹配。

确实,如果我写

std::string const s1 {"a"};

wrapper<std::string> w0{s1};  

我得到了值拷贝构造函数

第二个问题:我要做什么才能得到我想要的?

所以我必须做些什么来使值复制构造函数 (T const &) 获得优先于 emplace 构造函数 (As && ...) 的优先级也不是常量 T 值和, 大多数情况下,我必须做些什么才能让复制构造函数 (wrapper const &) 优先构造 w3?

如评论中所述,问题在于可变参数模板构造函数 通过转发引用获取参数,因此它更适合非 const 左值副本或 const 右值副本。

有很多方法可以禁用它,一种有效的方法是始终使用标签作为 in_place_t,如 SergeyA 在其答案中所建议的那样。另一种是在匹配复制构造函数签名时禁用模板构造函数,正如著名的 Effective C++ 书籍中所提出的那样。

在这种情况下,我更愿意为 copy/move 构造函数(以及 copy/move 赋值)声明所有可能的签名。这样,无论我向 class 添加任何新的构造函数,我都不必考虑避免复制构造,它只有 2 行代码,易于阅读并且不会污染其他构造函数的接口:

template <typename T>
struct wrapper
 {
   //...
   wrapper (wrapper& w0) : wrapper(as_const(w0)){}
   wrapper (const wrapper && w0) : wrapper(w0){}

 };

注意:如果计划将其用作 volatile 类型,或者满足以下所有条件,则不应使用此解决方案:

  • 对象大小小于 16 字节(对于 MSVC ABI 为 8 字节),
  • 所有成员子对象都可以轻松复制,
  • 这个包装器将被传递给函数,在这种情况下要特别注意参数是普通可复制类型且其大小小于先前阈值的情况,因为在这种情况下,可以传递参数通过按值传递参数在一个(或两个)寄存器中!

如果这些需求都满足了,那么你可以考虑实现可维护性较差(容易出错-> 下次修改代码)或客户端界面污染的解决方案!

按照 OP 的建议,将我的评论作为答案进行一些阐述。

由于执行重载解析和类型匹配的方式,可变参数前向引用类型的构造函数通常会被选为最佳匹配。发生这种情况是因为所有 const 限定都将被正确解析并形成完美匹配 - 例如,当将 const 引用绑定到非 const 左值等时 - 就像你的例子一样。

处理它们的一种方法是在参数列表与任何其他可用构造函数匹配(尽管不完美)时禁用(通过我们可以使用的各种 sfinae 方法)可变参数构造函数,但这非常乏味,并且需要持续每当添加额外的构造函数时都支持。

我个人更喜欢基于标签的方法,并使用标签类型作为可变参数构造函数的第一个参数。虽然任何标记结构都可以工作,但我倾向于(懒惰地)从 C++17 窃取一个类型 - std::in_place。代码现在变成:

template<class... ARGS>
Constructor(std::in_place_t, ARGS&&... args)

并且比称为

Constructor ctr(std::in_place, /* arguments */);

根据我在调用位置的经验,构造函数的性质始终是已知的 - 即,您将始终知道是否打算调用前向引用接受构造函数 - 这个解决方案对我来说很有效。

没有"constructor precedence rules,"构造函数在优先级方面没有什么特别之处。

这两个问题案例有相同的基本规则来解释它们:

wrapper<std::string> w0{s0};            // emplace constructor (?)
wrapper<std::string> w3{w0};            // compilation error (?)

对于w0,我们有两个候选者:值复制构造函数(采用std::string const&)和emplace构造函数(采用std::string&)。后者是更好的匹配,因为它的引用比值复制构造函数的引用(特别是 [over.ics.rank]/3)更少 cv 限定。一个较短的版本是:

template <typename T> void foo(T&&); // #1
void foo(int const&);                // #2

int i;
foo(i); // calls #1

类似地,对于 w3,我们有两个候选对象:emplace 构造函数(采用 wrapper&)和复制构造函数(采用 wrapper const&)。同样,由于相同的规则,首选 emplace 构造函数。这会导致编译错误,因为 value 实际上不能从 wrapper<std::string>.

构造

这就是为什么您必须小心转发引用并限制您的函数模板!这是 Effective Modern C++ 中的第 26 条 ("Avoid overloading on universal references") 和第 27 条 ("Familiarize yourself with alternatives to overloading on universal references")。最低限度为:

template <typename... As,
    std::enable_if_t<std::is_constructible<T, As...>::value, int> = 0>
wrapper(As&&...);

这允许 w3 因为现在只有一个候选人。 w0 放置而不是副本这一事实无关紧要,最终结果是相同的。实际上,值复制构造函数并没有真正完成任何事情——你应该删除它。


我会推荐这组构造函数:

wrapper() = default;
wrapper(wrapper const&) = default;
wrapper(wrapper&&) = default;

// if you really want emplace, this way
template <typename A=T, typename... Args,
    std::enable_if_t<
        std::is_constructible<T, A, As...>::value &&
        !std::is_same<std::decay_t<A>, wrapper>::value
        , int> = 0>
wrapper(A&& a0, Args&&... args)
  : value(std::forward<A>(a0), std::forward<Args>(args)...)
{ }

// otherwise, just take the sink
wrapper(T v)
  : value(std::move(v))
{ }

这样可以以最少的麻烦和混乱完成工作。请注意,emplace 和 sink 构造函数是互斥的,只能使用其中一个。