Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 841
Kyle Strand Avatar asked Jul 05 '16 23:07

Kyle Strand


People also ask

What is the purpose of virtual destructor?

A virtual destructor is used to free the space which is assigned to the object of the derived class while we are trying to delete the instances of the base class using a pointer object of the base class.

Why is virtual destructor used in C++?

Virtual destructors in C++ are used to avoid memory leaks especially when your class contains unmanaged code, i.e., contains pointers or object handles to files, databases or other external objects. A destructor can be virtual.

What are virtual constructors destructors?

Virtual Destructor : - The explicit destroying of object with the use of delete operator to a base class pointer to the object is performed by the destructor of the base-class is invoked on that object. - The above process can be simplified by declaring a virtual base class destructor.

Can we have virtual destructor in C++?

Yes, it is possible to have a pure virtual destructor. Pure virtual destructors are legal in standard C++ and one of the most important things to remember is that if a class contains a pure virtual destructor, it must provide a function body for the pure virtual destructor.


1 Answers

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;
};
like image 90
PcAF Avatar answered Oct 21 '22 01:10

PcAF