Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Template constructor vs. non-template constructor in class any

Tags:

c++

Imagine writing something like boost::any:

class any {
public:
  any();
  any(const any &);
  any(any &&);
  template<typename ValueType> any(const ValueType &);
  template<typename ValueType> any(ValueType &&);

Will the appropriate (copy/move) constructor be called for any possible any? Or does it have to be written with SFINAE e.g. like this:

template<typename ValueType,
  typename = typename std::enable_if<
    !std::is_same<any, typename std::decay<ValueType>::type>::value
  >::type>
  any(const ValueType& value)
template<typename ValueType,
  typename = typename std::enable_if<
    !std::is_same<any, typename std::decay<ValueType>::type>::value
  >::type>
  any(ValueType&& value)

The question is: Do I need to protect the templated constructor (to construct any from some value) or I can leave it, because the non-template (copy/move) constructor will always be matched for any? What about volatile modifier or some strange std::move((const any&)it) if that is possible?

Answer describing the search for the constructor will be most appriciated, thank you.

EDIT: Constructing any containing another any would be a problem, I definitelly want to avoid that (the SFINAE makes sure it cannot happen).

like image 742
firda Avatar asked Dec 15 '22 20:12

firda


2 Answers

With C++11 and the introduction of Universal Reference (and a constructor with such parameter) the rules of overload resolution will choose the templated version.

The truth is that if the compiler can choose between templated and non-templated function, it will go with the non-template. But it will do so only if they are equally good:

§ 13.3.3 Best viable function [over.match.best]

  1. [...] Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then

    — for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2), or, if not that,

    [...]

    — F1 is a non-template function and F2 is a function template specialization, [...]

That said, having two constructors declared like below:

any(const any &);

template <typename ValueType>
any(const ValueType &);

the compiler will choose the non-templated version, as instantiating the templated one would result with exactly same declaration.

However, with the constructor taking Unviersal Reference the situation changes radically:

any(const any &);

template <typename ValueType>
any(ValueType &&);

In the context of to copying an instance with regular direct-initialization syntax:

any a;
any b{a};

the evaluated type of a is an lvalue any & without the const modifier. After generating the set of candidate constructors for overload resolution the compiler ends up with following signatures:

any(const any &); // non-template

any(any &); // instantiated template

And then:

§ 13.3.1 Candidate functions and argument lists [over.match.funcs]

  1. In each case where a candidate is a function template, candidate function template specializations are generated using template argument deduction (14.8.3, 14.8.2). Those candidates are then handled as candidate functions in the usual way. A given name can refer to one or more function templates and also to a set of overloaded non-template functions. In such a case, the candidate functions generated from each function template are combined with the set of non-template candidate functions.

That is, the template version is a better match, and this is what the compiler chooses.

However, if one had:

const any a; // const!
any b{a};

Then this time the constructor signature generated from constructor taking Universal Reference would be the same as the non-template version of copy-constructor, so only then the non-template version is called.

What about volatile modifier or some strange std::move((const any&)it) if that is possible?

Exactly the same happens. The Universal Reference constructor is a better match.

That is, std::move((const any&)it) evaluates to expression of const any && type.

The parameter of non-template move constructor can take non-const rvalue reference (so does not match at all, since it lacks const modifier).

The parameter of non-template copy constructor can take the const lvalue reference (that is fine, const rvalue can be bound by const lvalue reference, but is not an exact match).

Then, the instantiated template taking Universal Reference is again a better match that will be invoked.

like image 79
Piotr Skotnicki Avatar answered Jan 05 '23 00:01

Piotr Skotnicki


As a general rule, if a template and a non-template function are otherwise equally good matches, the non-template version is chosen over the template version. Since your any copy/move constructors are non-template, for rvalues or constant lvalues they take precedence over the template constructors.

However thanks due the special rules for rvalue reference templates, the deduced type for template<typename ValueType> any(ValueType &&); will be any& which is a better match. Therefore when copying a non-const lvalue, you'll call the templated constructor.

Therefore you need an SFINAE rule for that templated constructor, but not for the templated constructor taking an lvalue reference to const.

like image 26
celtschk Avatar answered Jan 04 '23 23:01

celtschk