【问题标题】:Inlining an array of non-default constructible objects in a C++ class在 C++ 类中内联一组非默认可构造对象
【发布时间】:2014-08-31 04:45:20
【问题描述】:

C++ 不允许类包含不可默认构造的项目数组:

class Gordian {
  public:
    int member;
    Gordian(int must_have_variable) : member(must_have_variable) {}
};

class Knot {
  Gordian* pointer_array[8]; // Sure, this works.
  Gordian inlined_array[8]; // Won't compile. Can't be initialized.
};

即使是 C++ 初学者也知道,该语言保证在构造类时初始化所有非 POD 成员。而且它不信任用户初始化构造函数中的所有内容 - 必须在构造函数的主体开始之前向所有成员的构造函数提供有效参数。

总的来说,就我而言,这是一个好主意,但我遇到过这样一种情况,如果我实际上可以拥有一组非默认可构造对象,那会容易得多。

显而易见的解决方案:拥有一个指向对象的指针数组。在我的情况下,这不是最佳选择,因为我使用的是共享内存。这将迫使我从已经竞争的资源(即共享内存)中进行额外分配。我希望在对象中内联数组的全部原因是减少分配的数量。

在这种情况下,我愿意使用 hack,即使是丑陋的,只要它有效。我正在考虑的一种可能的技巧是:

class Knot {
  public:
    struct dummy { char padding[sizeof(Gordian)]; };
    dummy inlined_array[8];
    Gordian* get(int index) {
      return reinterpret_cast<Gordian*>(&inlined_array[index]);
    }
    Knot() {
      for (int x = 0; x != 8; x++) {
        new (get(x)) Gordian(x*x);
      }
    }
};

当然,它可以编译,但我并不是一个经验丰富的 C++ 程序员。也就是说,我不可能少相信我的黑客。那么问题来了:

1) 我想出的破解方法看起来可行吗?有哪些问题? (我主要关心新版本 GCC 上的 C++0x)。

2) 有没有更好的方法来内联类中的非默认可构造对象数组?

【问题讨论】:

  • 代码合法。但是,我认为将非 POD C++ 对象放在共享内存中是一个糟糕的设计。
  • 我认为您当前的解决方案还可以。您可能希望将 vector 与自定义分配器一起使用,该分配器将从您准备的缓冲区分配内存,但如果您不想在此缓冲区中预分配任何额外内存,则这种方法 afaik 不起作用。
  • “语言保证在构造类时初始化所有成员”的说法是不正确的。 C++ 不保证 POD 类型(整数、C 风格结构等)的初始化。
  • @Cantos:哦,你 :) 但是,是的,当我在做陈述时,我不妨正确地做。编辑修复。
  • (多年后)旁注:“hack”很可能是 C++11 及更高版本中未定义的行为。 std::aligned_storage - cppreference.com 。另见c++ - Initialize array of non default constructible objects of template argument length - Stack Overflow

标签: c++ constructor


【解决方案1】:

一方面,您可以使用数组包装器(例如boost::array)用固定值初始化数组:

#include <boost/array.hpp>

class Gordian {
public:
    int member;
    Gordian(int must_have_variable) : member(must_have_variable) {}
};

namespace detail
{
    boost::array<Gordian, 8> chop()
    {
        boost::array<Gordian, 8> t = {{0, 1, 4, 9, 16, 25, 36, 49}};
        return t;
    }
}

class Knot {
    boost::array<Gordian, 8> array;
public:
    Knot(): array(detail::chop()) {}
};

另一种可能性是boost::optional 的数组(但会有一些大小开销):

#include <boost/optional.hpp>

class Gordian {
public:
    int member;
    Gordian(int must_have_variable) : member(must_have_variable) {}
};

class Knot {
    boost::optional<Gordian> array[8];
public:
    Knot()
    {
        for (int x = 0; x != 8; x++) {
            array[x] = Gordian(x*x);
        }
    }
};

【讨论】:

  • 谁能告诉我为什么这个答案被否决了?对我来说,这似乎是一个有趣的可能性。有什么问题吗?
  • 实际上,使用 boost optional 可能是最简单的方法。几个字节的开销不是问题,尤其是当它使我免于调用共享内存分配器两次时。
【解决方案2】:

内存对齐可能会中断,因为Knot 认为它只包含字符。除此之外,这个技巧是可行的。我见过的另一个更通用的技巧是提供插入成员函数,这些函数返回原始内存以由调用者填充,例如:

SuperOverOptimisedVector<Gordian> v;
...
Gordian* g = v.append();
new (g) Gordian(42);

外观可能具有欺骗性,所以我会解释一下。 v.append() 函数不会从堆中分配原始内存。它只是在向量中找到下一个可用槽(如果容量耗尽,则调整大小并复制,与 std::vector 相同)并将该槽的地址传回给调用者。

这个技巧虽然可爱又聪明,但有些古怪且容易出错。遵守一步式惯例可以部分缓解这种情况:

new (v.append()) Gordian(42);

但我更愿意将其视为一种有趣的好奇心,通常应该避免。

总而言之,是的,您可以将不可默认构造的对象存储在连续数组中,但除非性能差异大到足以影响项目的成功,否则请使用 std::vector。

【讨论】:

    【解决方案3】:

    根据我得到的答案和我最初的 hack,我想出了这个使用 boost::aligned_storage 的通用解决方案。基本上是 void 类型,但用于结构。

    class Gordian {
      public:
        int member;
        Gordian(int must_have_variable) : member(must_have_variable) {}
    };
    
    template <class T>
    struct VoidWrap {
      boost::aligned_storage<sizeof(T)> storage;
      T* address() { return reinterpret_cast<T*>(storage.address()); }
    };
    
    class Knot {
      public:
        VoidWrap<Gordian> void_gordian[8];
        Knot() {
          for (int x = 0; x != 8; x++) {
            new (void_gordian[x].address()) Gordian(x*x);
          }
        }
    };
    

    或者,专门针对以下用例的扩展版本 1) 在包含它的对象的构造中进行初始化。 2) 访问。 3) 可能重新分配值。 4) 适当的自动销毁。 (当然,如果在没有初始化的情况下被销毁/访问,它可能会爆炸)

    template <class T>
    struct VoidWrap {
      boost::aligned_storage<sizeof(T)> storage;
      /// Returns an address on which to construct the object. Use only once.
      T* construct() { return access(); }
      /// access should only be called on a `construct`ed object
      /// obj.access() is equivalent to &obj
      T* access() { return reinterpret_cast<T*>(this); }
      /// Assumes the object has been constructed. Calls the destructor on the old
      /// object, then returns an address on which to construct a new object.
      T* reconstruct() {
        access()->~T();
        return access(); 
      }
      ~VoidWrap() {
        access()->~T();
      }
    };
    
    VoidWrap<Gordian> void_g;
    new (void_g.construct()) Gordian(10);
    cout << void_g.access()->member << endl;
    new (void_g.reconstruct()) Gordian(20);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-07-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-06
      • 1970-01-01
      相关资源
      最近更新 更多