今天我发现我不了解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
w1
,w2
和w4
的值分别使用预期的值移动构造函数,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 &
?
答案 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){}
};
注意:如果计划将其用作易失性类型,或者满足以下所有条件,则不应使用此解决方案:
如果所有这些要求都得到满足,那么您可能会考虑实施可维护性较低(容易出错的错误->下次将修改代码)或客户端接口污染解决方案!