虚拟析构函数会改变 decltype 的行为

Virtual destructor alters behavior of decltype

本文关键字:decltype 改变 析构函数 虚拟      更新时间:2023-10-16

我已经为可选的惰性参数创建了一个标头(在GitHub存储库中也可见)。(这不是我基于标题的第一个问题。

我有一个基类模板和两个派生类模板。基类模板具有一个带有static_assertprotected构造函数。此构造函数仅由特定的派生类调用。在static_assert里面,我正在使用decltype.

真正奇怪的是,decltype 中名称的类型在某种程度上受到我的基类模板中是否存在虚拟析构函数的影响。

这是我的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;}));
}

请注意,如果析构函数被注释掉,则编译时不会出错。

目的是使callable成为类型 U 的表达式,当使用 () 运算符调用该表达式时,返回类型 T 的内容。如果没有 Base 中的虚拟析构函数,这似乎是正确评估的;使用虚拟析构函数,似乎callabele的类型是Base<T>(据我所知,这毫无意义)。

下面是 G++ 5.1 的错误消息:

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

这是 Clang++ 3.7 的错误消息:

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.

这是一个在线版本。

编辑:=delete复制构造函数也会触发此错误。

问题是当你声明析构函数时,隐式移动构造函数不会被声明,因为

(N4594 12.8/9)

如果类 X 的定义没有显式声明移动构造函数,则非显式构造函数将是隐式的 声明为默认值当且仅当

  • X 没有用户声明的析构函数

Base具有用户声明的析构函数(默认无关紧要)。

MakeLazyInt尝试返回构造Derived对象时,它会调用Derived移动构造函数。

隐式声明Derived移动构造函数不会调用Base移动构造函数(因为它不存在),而是调用模板化的Base(U&&)构造函数。

问题是,callable参数不包含可调用对象,而是Base对象,它实际上不包含operator ()

要解决这个问题,只需在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;
};