Search code examples
c++templatesc++14decltypevirtual-destructor

Virtual destructor alters behavior of decltype


I've created a header for optionally-lazy parameters (also visible in a GitHub repository). (This is not my first question based on the header.)

I have a base-class template and two derived-class templates. The base-class template has a protected constructor with a static_assert. This constructor is only called by a particular derived-class. Inside of the static_assert I'm using a decltype.

The really bizarre thing is that the type of a name inside the decltype is somehow affected by the whether or not there is a virtual destructor in my base-class template.

Here's my MCVE:

#include <type_traits>
#include <utility>

template <typename T>
class Base
{
  protected:
    template <typename U>
    Base(U&& callable)
    {
      static_assert(
          std::is_same<
              typename std::remove_reference<decltype(callable())>::type, T
            >::value,
          "Expression does not evaluate to correct type!");
    }

  public:
    virtual ~Base(void) =default; // Causes error 

    virtual operator T(void) =0;
};

template <typename T, typename U>
class Derived : public Base<T>
{
  public:
    Derived(U&& callable) : Base<T>{std::forward<U>(callable)} {}

    operator T(void) override final
    {
      return {};
    }
};

void TakesWrappedInt(Base<int>&&) {}

template <typename U>
auto MakeLazyInt(U&& callable)
{
  return Derived<
            typename std::remove_reference<decltype(callable())>::type, U>{
      std::forward<U>(callable)};
}

int main()
{
  TakesWrappedInt(MakeLazyInt([&](){return 3;}));
}

Note that if the destructor is commented out, this compiles without error.

The intent is for callable to be an expression of type U that, when called with the () operator, returns something of type T. Without the virtual destructor in Base, it appears that this is evaluated correctly; with the virtual destructor, it appears that callabele's type is Base<T> (which, as far as I can tell, makes no sense).

Here's G++ 5.1's error message:

recursive_lazy.cpp: In instantiation of ‘Base<T>::Base(U&&) [with U = Base<int>; T = int]’:
recursive_lazy.cpp:25:7:   required from ‘auto MakeLazyInt(U&&) [with U = main()::<lambda()>]’
recursive_lazy.cpp:48:47:   required from here
recursive_lazy.cpp:13:63: error: no match for call to ‘(Base<int>) ()’
               typename std::remove_reference<decltype(callable())>::type, T

Here's Clang++ 3.7's error message:

recursive_lazy.cpp:13:55: error: type 'Base<int>' does not provide a call operator
              typename std::remove_reference<decltype(callable())>::type, T
                                                      ^~~~~~~~
recursive_lazy.cpp:25:7: note: in instantiation of function template specialization
      'Base<int>::Base<Base<int> >' requested here
class Derived : public Base<T>
      ^
1 error generated.

Here is an online version.

EDIT: =delete-ing the copy-constructor also triggers this error.


Solution

  • The problem is that when you declare destructor, implicit move constructor won't be declared, because

    (N4594 12.8/9)

    If the definition of a class X does not explicitly declare a move constructor, a non-explicit one will be implicitly declared as defaulted if and only if

    ...

    • X does not have a user-declared destructor

    Base has user-declared destructor (it doesn't matter that it's defaulted).

    When MakeLazyInt tries to return constructed Derived object, it calls Derived move constructor.

    Derived implicitly-declared move constructor doesn't call Base move constructor (because that doesn't exist), but rather your templated Base(U&&) constructor.

    And here's the problem, callable parameter doesn't contain callable object but Base object, which really doesn't contain operator ().

    To solve the problem simply declare move constructor inside Base:

    template <typename T>
    class Base
    {
      protected:
        template <typename U>
        Base(U&& callable)
        {
          static_assert(
              std::is_same<
                  typename std::remove_reference<decltype(callable())>::type, T
                >::value,
              "Expression does not evaluate to correct type!");
        }
    
      public:
        virtual ~Base(void) =default; // When declared, no implicitly-declared move constructor is created
    
        Base(Base&&){} //so we defined it ourselves
    
        virtual operator T(void) =0;
    };