简体   繁体   中英

Constructor design for a class which delegates a value to one of its member variables

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 a tree(T const&) , tree(T&&) combination. But : The compiler will take the template<typename U> tree(U&&) ctor in push_back , where the intend is that the move ctor of tree 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM