Please consider the following tree
class
template<typename T>
class tree
{
public:
template<typename U>
tree(U&& value)
: m_value(std::forward<U>(value))
{ }
private:
T m_value;
};
and some verbose class foo
which is constructed by its std::string name
which is printed via std::cout
whenever one of its constructors is invoked.
foo a("a"), b("b");
foo const c("c");
tree<foo> s(std::move(a)), t(b), u(c);
yields the following output:
a constructed
b constructed
c constructed
a moved
b copied
c copied
which is as expected.
How does this work?
To be specific: We cannot use
tree(T&& value)
: m_value(std::forward<T>(value))
{ }
Why ? Well, cause we force decltype(value)
to be foo&&
which is not the case in t(b)
and u(c)
. We would need to provide
tree(T const& value)
: m_value(value)
{ }
as well, if we want to use this arguable variant. The ctor used in the tree
implementation works cause of the reference collapsing rules .
But why can't we use
template<typename U> tree(U&& value) : m_value(std::forward<T>(value)) { }
instead?
Well, the reason is that in u(c)
we have U = foo const& = decltype(value)
, but std::forward<T>
only accepts foo&
or foo&&
, since T = foo
. In contrast, std::forward<U>
accepts foo const&
(or foo const&&
), since U = foo const&
.
So, if I'm not overlooking something, we should use
template<typename U> tree(U&&)
instead of atree(T const&)
,tree(T&&)
combination. But : The compiler will take thetemplate<typename U> tree(U&&)
ctor inpush_back
, where the intend is that the move ctor oftree
is taken:
foo a("a"), b("b");
foo const c("c");
tree<foo> t("root");
t.emplace_back(std::move(a));
t.emplace_back(b);
t.emplace_back(c);
tree<foo> u("other root");
u.push_back(t); // cannot convert argument 1 from 'tree<foo>' to 'std::string const&'
What can I do? Do I need to use the
tree(T const&)
,tree(T&&)
combination instead?
[Sorry for being a bit too verbose, but I felt responsible for clarifying any technical issue before asking for design.]
A greedy constructor such as template<typename U> tree(U&&)
must be suitably constrained, or you'll have a lot of problems down the road. You saw it hijack copy construction from a non-const lvalue because it's a better match than the copy constructor. It also defines an implicit conversion from everything under the sun, which can have "fun" effects on overload resolution.
A possible constraint might be "accept only things that are convertible to T
":
template<typename U, class = std::enable_if_t<std::is_convertible_v<U, T>>>
tree(U&& u) : m_value(std::forward<U>(u)) {}
Or perhaps "accept only things that are convertible to T
and not a tree
".
template<class U>
using not_me = std::negation<std::is_same<U, tree>>;
template<typename U,
class = std::enable_if_t<std::conjunction_v<not_me<std::decay_t<U>>,
std::is_convertible<U, T>>>>
tree(U&& u) : m_value(std::forward<U>(u)) {}
You can do both.
If you go with:
template<typename U>
tree(U&& value)
: m_value(std::forward<U>(value))
{ }
the downside is that any one-argument constructor of T
can be invoked this way. This may not be what you want. For example, with that variant, the following is valid:
struct foo
{
foo() = default;
foo(const foo &ref) = default;
explicit foo(int);
};
tree<foo> t(10);
t = 20;
This is a decision you need to find for yourself, however, I personally see that as a huge downside. I would make that constructor explicit
(eliminating the second initialisation) and go for tree(const T&)
along with tree(T&&)
to avoid the first initialisation.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.