值包装器的可变参数模板构造函数的类构造函数优先级

时间:2018-08-20 19:41:49

标签: c++ c++11 templates variadic-templates constructor-overloading

今天我发现我不了解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方法为例)。

我的意图是使用通过包装器调用的copy或move构造函数,通过T对象传递值的copy或move构造函数,并使用能够构造类型对象的值列表调用模板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的值分别使用预期的值移动构造函数,Emplace构造函数和move构造函数构造。

但是w0是使用emplace构造函数构造的(我期望是值复制构造函数),w3根本没有构造(编译错误),因为首选emplace构造函数,而不是{接受std::string值的{1}}构造函数。

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

我认为wrapper<std::string>问题是因为w0不是s0值,所以const不是精确匹配。

的确,如果我写

T const &

我得到的值复制构造函数称为

第二个问题:要获得想要的东西我必须做什么?

所以我需要做的是使值复制构造函数(std::string const s1 {"a"}; wrapper<std::string> w0{s1}; )优先于emplace构造函数(T const &),并且使用的不是恒定的As && ...值,而且大多数情况下,如何使副本构造函数(T)优先构建wrapper const &

3 个答案:

答案 0 :(得分:6)

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

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

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&)和copy构造函数(采用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构造函数是互斥的,请使用其中之一。

答案 1 :(得分:4)

正如OP所建议的那样,将我的评论作为详尽的回答。

由于执行重载解析和匹配类型的方式,通常会选择可变的前向引用构造函数类型作为最佳匹配。之所以会发生这种情况,是因为所有const限定符都将被正确解析并形成完美的匹配-例如,将const引用绑定到非const lvalue等时(例如您的示例)。

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

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

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

比称为

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

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

答案 2 :(得分:1)

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

有很多禁用它的方法,一种有效的方法是始终使用SergeyA在其答案中建议的标记in_place_t。另一种是禁用模板构造函数,使其与著名的《 Effective C ++》书中提出的复制构造函数的签名相匹配。

在这种情况下,我更喜欢为复制/移动构造函数(以及复制/移动分配)声明所有可能的签名。这样,无论我在类中添加什么新的构造函数,我都不必考虑避免复制构造,它是短短的两行代码,易于阅读,并且不会污染其他构造函数的接口:

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

 };

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

  • 对象大小小于16字节(对于MSVC ABI,则小于8字节),
  • 所有成员主题都是可复制的,
  • 此包装器将传递给函数,在这种情况下,如果参数为普通可复制类型且其大小小于先前的阈值,则应格外小心,因为在这种情况下,可以在通过传递参数值来注册(或两个)!

如果所有这些要求都得到满足,那么您可能会考虑实施可维护性较低(容易出错的错误->下次将修改代码)或客户端接口污染解决方案!