首页 > 解决方案 > 值包装器的类构造函数优先级与可变参数模板构造函数

问题描述

今天我发现我不懂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) 和一种模板就地构造值构造函数,以 STL 容器As && ... as的方法为例)。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

,w1和值是按预期使用值移动构造函数,安放构造函数和移动构造函数(分别)构造的w2w4

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

标签: c++c++11templatesvariadic-templatesconstructor-overloading

解决方案


没有“构造函数优先规则”之类的东西,构造函数在优先级方面没有什么特别之处。

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

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

对于w0,我们有两个候选者:值复制构造函数(接受 a std::string const&)和 emplace 构造函数(接受 a 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 构造函数(接受 a wrapper&)和复制构造函数(接受 a wrapper const&)。同样,由于相同的规则,emplace 构造函数是首选。这会导致编译错误,因为value实际上不能从wrapper<std::string>.

这就是为什么你必须小心转发引用并限制你的函数模板!这是有效现代 C++ 中的第 26 条(“避免在通用引用上重载”)和第 27 条(“让自己熟悉在通用引用上重载的替代方法”)。最低限度是:

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 构造函数是互斥的,只使用其中一个。


推荐阅读