Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

decltype() variadic template base class

Tags:

c++

c++11

c++14

I have the following code where I'm expecting decltype() to not work on Derived class to get run() base class method return-type, since the base class does not have a default constructor.

class Base
{
  public:
    int run() { return 1; }

  protected:
    Base(int){}
}; 

struct Derived : Base
{
  template <typename ...Args>
  Derived(Args... args) : Base{args...}
  {}
};

int main()
{
  decltype(Derived{}.run()) v {10}; // it works. Not expected since
                                    // Derived should not have default constructor
  std::cout << "value: " << v << std::endl;

  //decltype(Base{}.run()) v1 {10}; // does not work. Expected since 
                                    // Base does not have default constructor
  //std::cout << "value: " << v1 << std::endl;
}

I'm aware you can use declval<> to get member functions without going through constructors, but my question is why decltype works here. I tried to find in the C++ standard something relevant, but did not find anything. Also tried multiple compilers (gcc 5.2, 7.1 and clang 3.8) and have the same behavior.

like image 588
AdvSphere Avatar asked Feb 01 '18 00:02

AdvSphere


People also ask

What does decltype do in c++?

The decltype type specifier yields the type of a specified expression. The decltype type specifier, together with the auto keyword, is useful primarily to developers who write template libraries. Use auto and decltype to declare a function template whose return type depends on the types of its template arguments.

Why to use decltype?

You should use decltype when you want a new variable with precisely the same type as the original variable. You should use auto when you want to assign the value of some expression to a new variable and you want or need its type to be deduced.


2 Answers

Behold the magic of unevaluated contexts... and lying.

Actually trying to do something like:

Derived d;

will be a compile error. It's a compiler error because in the process of evaluating Derived::Derived() we have to invoke Base::Base(), which doesn't exist.

But that's a detail of the implementation of the constructor. In an evaluated context, we certainly need to know that. But in an unevaluated context, we don't need to go so far. If you examine std::is_constructible<Derived>::value, you'd see that is true! This is because you can instantiate that constructor with no arguments - because the implementation of that constructor is outside of the immediate context of that instantiation. This lie - that you can default-construct Derived - allows you to use Derived{} in this context, and the compiler will happily allow you to go on your merry way and see that decltype(Derived{}.run()) is int (which also does not involve actually invoking run, so the body of that function is likewise irrelevant).

If you were honest in the Derived constructor:

template <typename ...Args,
    std::enable_if_t<std::is_constructible<Base, Args&...>::value, int> = 0>
Derived(Args&... args) : Base(args...) { }

Then decltype(Derived{}.run()) would fail to compile, because now Derived{} is ill-formed even in an unevaluated context.

It's good to avoid lying to the compiler.

like image 131
Barry Avatar answered Sep 22 '22 19:09

Barry


When an expression inside decltype involves a function template, the compiler only looks at the template function's signature to determine whether or not the template could be instantiated if the expression were really in an evaluated context. The actual definition of the function is not used at that point.

(In fact, this is why std::declval can be used inside decltype even though std::declval has no definition at all.)

Your template constructor has the same signature as though simply declared but not yet defined:

template <typename ...Args>
Derived(Args&... args);

While processing the decltype, the compiler just looks at that much information and decides Derived{} is a valid expression, an rvalue of type Derived<>. The : Base{args...} part is part of the template definition and doesn't get used inside decltype.

If you want a compiler error there, you could use something like this to make your constructor more "SFINAE-friendly", which means information about whether or not a specialization of the template is actually valid gets put into the template signature.

template <typename ... Args, typename Enable =
    std::enable_if_t<std::is_constructible<Base, Args&...>::value>>
Derived( Args& ... args ) : Base{ args... }
{}

You might also want to modify the constructor to avoid "too perfect forwarding". If you do Derived x; Derived y{x};, the template specialization Derived(Derived&); will be a better match than the implicit Derived(const Derived&);, and you'll end up trying to pass x to Base{x} rather than using the implicit copy constructor of Derived.

like image 24
aschepler Avatar answered Sep 21 '22 19:09

aschepler