用指针替换多态性的未定义行为

Undefined behavior in replacement for polymorphism with out Pointers

我最近在 this question 上问过这个问题。我已经采用了这种通用方法:

#define COFFEE_GEN_GENERIC_VALUE(CLASS_TYPE) using GenericValue##CLASS_TYPE = coffee::utils::GenericValue<CLASS_TYPE, COFFEE_GENERIC_VALUE_CONFIG_MAX_DERIVED_SIZE_OF_##CLASS_TYPE>

template<typename Base, size_t MaxDerivedSize>
class GenericValue
{
private:
    char buffer[MaxDerivedSize];

public:
    GenericValue() = default;

    template<typename Derived, typename... ConstructorArgs>
    static GenericValue<Base, MaxDerivedSize> create(ConstructorArgs... args)
    {
        static_assert(std::is_base_of<Base, Derived>::value, "Derived must derive from Base.");
        static_assert(sizeof(Derived) <= MaxDerivedSize, "The size specified with the macro COFFEE_GenericValueMaxSize is to small.");
        GenericValue<Base, MaxDerivedSize> result{};
        new ((Derived*) result.buffer) Derived{args...};
        return result;
    }

    template<typename Derived>            
    static GenericValue<Base, MaxDerivedSize> from(Derived* toCopy)
    {
        static_assert(std::is_base_of<Base, Derived>::value, "Derived must derive from Base.");
        static_assert(sizeof(Derived) <= sizeof(MaxDerivedSize), "The size specified with the macro COFFEE_GenericValueMaxSize is to small.");
        GenericValue<Base, MaxDerivedSize> result{};
        memcopy(result.buffer, toCopy, sizeof(Derived));
        return result;
    }
    
    GenericValue(const GenericValue<Base, MaxDerivedSize>& other)
    {
        memcopy(buffer, other.buffer, MaxDerivedSize);
    }
    
    GenericValue(GenericValue<Base, MaxDerivedSize>&& other)
    {
        memcopy(buffer, other.buffer, MaxDerivedSize);
    }

    GenericValue<Base, MaxDerivedSize>& operator=(const GenericValue<Base, MaxDerivedSize>& other)
    {
        memcopy(buffer, other.buffer, MaxDerivedSize);
        return *this;
    }
    
    GenericValue<Base, MaxDerivedSize>& operator=(GenericValue<Base, MaxDerivedSize>&& other)
    {
        coffee::utils::copy(buffer, other.buffer, MaxDerivedSize);
        return *this;
    }

    Base& operator*()
    {
        return *(Base*)buffer;
    }

    Base* operator->()
    {
        return (Base*)buffer;
    }
}

我想到了这种形式 this website,它与 @Matthias Grün 对我的老问题的遮阳篷有点相似。 您使用 COFFEE_GEN_GENERIC_VALUE 宏为 GenericValue<Base, MaxDerivedSize> 创建 typedef,大小将通过第二个宏传递给它。这样我的库就可以生成将用于特定基础 class 的适当 GenericValue 类型。它背后的基本思想是将派生类型的整个实例及其 vtable 存储在 buffer 中。您可以像复制常规右值一样复制它。

不幸的是,复制只在某些时候有效,我不知道为什么,据我所知,这不是问题。如果我删除复制运算符和构造函数并仅使用 class 作为 new 的包装器,它会很好用(除了对象没有真正复制)。

有人知道问题出在哪里吗?

我将 post 一个具体的例子,当我发现一个边缘情况时,我不必 post 我的整个项目。

memcpy 不是“普通旧数据”的数据是未定义的行为。当你玩恶作剧时,避免未定义的行为。

所以你需要做的第一件事是在你的缓冲区旁边存储一个指向复制操作的指针。然后当你复制你的缓冲区时,你调用指针复制操作而不是 memcpy。

一个简单的方法是使用 void(*)(void const* src, void* dst) 函数指针从 src 复制到 dst

将内存转换为不是它的类型并使用该指针是未定义的行为。

因此,您需要做的第二件事是存储一种从内存缓冲区获取指向基址的指针的方法。一个简单的方法是存储一个 Base*(*)(void*) 函数指针,并将它传递给缓冲区的指针。

对于一些间接的成本,您可以将每个实例的开销减少到像这样的单个指针。

struct BasicVTable {
  void(* destroy)(void*) = 0;
  template<class T>
  static BasicVTable make() {
    return {[](void* ptr){ static_cast<T*>(ptr)->~T(); }};
  }
  template<class T>
  static auto const* get() {
    static const auto vtable = make<T>();
    return &vtable;
  }
};
struct RegularVTable : BasicVTable {
  void(*assign)(void const* src, void* dst) = 0;
  void(*assign_move)(void* src, void* dst) = 0;
  void(*copy)(void const* src, void* dst) = 0;
  void(*move)(void* src, void* dst) = 0;

  
  template<class T>
  static RegularVTable make() {
    return {
      BasicVTable::template make<T>(),
      [](void const* src, void* dst) {
        *static_cast<T*>(dst) = *static_cast<T const*>(src);
      },
      [](void* src, void* dst) {
        *static_cast<T*>(dst) = std::move(*static_cast<T*>(src));
      },
      [](void const* src, void* dst) {
        ::new(dst) T(*static_cast<T const*>(src));
      },
      [](void* src, void* dst) {
        ::new(dst) T(std::move(*static_cast<T*>(src)));
      }
    };
  }
  template<class T>
  static auto const* get() {
    static const auto vtable = make<T>();
    return &vtable;
  }
};

template<class Base>
struct PolyBaseVTable : RegularVTable {
  Base*(*GetBase)(void* src) = 0;
  Base const*(*cGetBase)(void const* src) = 0;

  template<class T>
  static PolyBaseVTable make() {
    return {
      RegularVTable::make<T>(),
      [](void* src)->Base* {
        return static_cast<T*>(src);
      },
      [](void const* src)->Base const* {
        return static_cast<T const*>(src);
      }
    };
  }

  template<class T>
  static auto const* get() {
    static const auto vtable = make<T>();
    return &vtable;
  }
};

现在我们修改您的 GenericValue:

template<typename Base, size_t MaxDerivedSize>
class GenericValue
{
private:
  PolyBaseVTable<Base> const* vtable = nullptr;
  char buffer[MaxDerivedSize];

存储一个额外的指针。

我们做了一些细微的修改:

template<typename Derived, typename... ConstructorArgs>
static GenericValue<Base, MaxDerivedSize> create(ConstructorArgs... args)
{
    static_assert(std::is_base_of<Base, Derived>::value, "Derived must derive from Base.");
    static_assert(sizeof(Derived) <= MaxDerivedSize, "The size specified with the macro COFFEE_GenericValueMaxSize is to small.");
    GenericValue<Base, MaxDerivedSize> result{};
    ::new ((void*) result.buffer) Derived{args...}; // void* here
    result.vtable = PolyBaseVTable<Base>::template get<Derived>();
    return result;
}

我们在构造对象后初始化 vtable

你的from是胡说八道。这只是另一个 create.

然后复制结构是:

GenericValue(const GenericValue<Base, MaxDerivedSize>& other)
{
  if (!other.vtable) return;
  other.vtable->copy( &other.buffer, &buffer );
  vtable = other.vtable;
}

与其他 assignment/move 操作类似。分配有点痛苦:

GenericValue& operator=(const GenericValue& other)
{
  if (vtable != other.vtable)
  {
    if (vtable) vtable->destroy(&buffer);
    vtable = nullptr;
    if (other.vtable)
    {
        other.vtable->copy(&other.buffer, &buffer);
        vtable=other.vtable;
        return *this;
    }
  }
  if (!vtable) return *this;
  vtable->assign( &other.buffer, &buffer );
  return *this;
}

因为您必须处理类型更改的可能性。

对于基础访问:

Base* operator->()
{
  if (!vtable) return nullptr;
  return vtable->GetBase( &buffer );
}
Base const* operator->() const
{
  if (!vtable) return nullptr;
  return vtable->cGetBase( &buffer );
}

简单易行。

现在,此代码 确实 使用了 , so the static make functions in the vtable will have to be modified to be more verbose if you are restricted to 之后的扩展聚合规则。但是那里没有什么真正棘手的。

此外,自动 return 类型推导(我在一些地方使用 auto )可能必须替换为实际类型。预期的类型并不难解决,我只是没有,因为我不想在那里重复自己。

当然,您还需要在 ~GenericValue 上调用 vtable->destroy


现在,您可能会注意到我将 table 操作设为 vtable。这是因为我正在以 C-with-enhanced-C++ 代码生成方式重新实现基于经典 C++ 的多态性的简单版本。

您实际上可以使用它来实现转换为基础以外的操作,以避免额外的 vtable 命中。我只会在检测到额外 vtable 命中的性能,或者当我想存储异构对象(如 std::function 的工作方式)并支持它们的多态性时才考虑这一点。

Live example 正在运行中。

C++11 version.