C++ 抑制自动初始化和销毁

5

如何抑制类型的自动初始化和销毁?虽然T buffer[100]自动初始化了buffer的所有元素,并在它们超出作用域时销毁它们,但这并不是我想要的行为。

#include <iostream>

static int created   = 0,
           destroyed = 0;

struct S
{
    S()
    {
        ++created;
    }
    ~S()
    {
        ++destroyed;
    }
};

template <typename T, size_t KCount>
class fixed_vector
{
private:
    T m_buffer[KCount];
public:
    fixed_vector()
    {
        // some way to suppress the automatic initialization of m_buffer
    }

    ~fixed_vector()
    {
        // some way to suppress the automatic destruction of m_buffer
    }
};

int main()
{
    {
        fixed_vector<S, 100> arr;
    }

    std::cout << "Created:\t"   << created   << std::endl;
    std::cout << "Destroyed:\t" << destroyed << std::endl;
    return 0;
}

这个程序的输出是:
Created:    100
Destroyed:  100

我希望您能将其变为以下内容:

我希望它是:

Created:    0
Destroyed:  0

我的唯一想法是将 m_buffer 设计成一种像 char 这样的可以轻松构造和析构的类型,然后依赖于 operator[] 来为我封装指针运算,虽然这似乎是一个可怕的 hack 解决方案。另一个解决方案是使用 mallocfree,但这会给我带来不必要的间接性。


我之所以想这样做,是因为我正在制作一个容器,我不想为我不使用的东西支付初始化开销。例如,如果我的 main 函数是:

int main()
{
    {
        std::vector<S> vec;
        vec.reserve(50);
    }

    std::cout << "Created:\t"   << created   << std::endl;
    std::cout << "Destroyed:\t" << destroyed << std::endl;
    return 0;
}

输出将是正确的:
Created:    0
Destroyed:  0

所以如果我理解你的意思正确的话,你想为向量中的元素预留空间,以便稍后填充,而且你不想为构造(和析构)元素付出两倍的代价。这个性能差异真的那么重要吗?你有对你的应用程序进行分析并验证在执行期间是否花费了最多的时间吗? - Péter Török
为什么不直接使用指针向量而不是对象向量呢? - chalup
@Péter:我的问题更多地涉及没有无参构造函数的对象的构建。即使有了解决方案,具有昂贵构造函数或没有赋值运算符的对象仍然是一个问题。 - Travis Gockel
@chalup:因为那是一种极其丑陋的间接层级,其性能与我的解决方案相差甚远。 - Travis Gockel
编写一个没有构造函数的基类和一个带有构造函数的派生类。 - Hans Passant
显示剩余5条评论
5个回答

4

您可能需要了解 boost::optional

template <typename> struct tovoid { typedef void type; };

template <typename T, size_t KCount, typename = void>
struct ArrayStorage {
  typedef T type;
  static T &get(T &t) { return t; }
};

template <typename T, size_t KCount>
struct ArrayStorage<T, KCount, typename tovoid<int T::*>::type> {
  typedef boost::optional<T> type;
  static T &get(boost::optional<T> &t) {
    if(!t) t = boost::in_place();
    return *t;
  }
};

template <typename T, size_t KCount>
class Array
{
public:
    T &operator[](std::ptrdiff_t i) {
      return ArrayStorage<T, KCount>::get(m_buffer_[i]);
    }

    T const &operator[](std::ptrdiff_t i) const {
      return ArrayStorage<T, KCount>::get(m_buffer_[i]);
    }

    mutable typename ArrayStorage<T, KCount>::type m_buffer_[KCount];
};

专门针对包装在optional中的类类型进行特化,从而实现惰性调用构造函数/析构函数。对于非类类型,我们不需要进行包装。不进行包装意味着我们可以将&a[0]视为连续的内存区域,并将该地址传递给需要数组的C函数。boost::in_place会就地创建类类型,而不使用临时T或其复制构造函数。

不使用继承或私有成员使得该类能够保持聚合状态,从而允许一种方便的初始化形式。

// only two strings are constructed
Array<std::string, 10> array = { a, b };

这实际上是我首先研究的事情。然而,boost::optional<T> 有一个额外的标志来赋予它使用 operator bool() 的能力,因此 lazy_array<int> 将不必要地占用双倍的空间。 - Travis Gockel
@Travis,你要么追求速度,要么追求空间。我认为这个问题不能没有标志位来解决。你怎么知道 m_buffer[i] 还没有被构造出来?放入该位置的元素可以建立任何可能的位组合,因此你无法仅通过该内存单元的纯粹手段确定其状态。但实际上,只需要一个 bool 就可以了。如果你追求空间,可以分配一个 T* 数组并将其初始化为空指针,然后使用 new 来分配你的元素。但是我甚至不确定这是否会在堆管理方面使用更少的空间。 - Johannes Schaub - litb
我想我在最初的问题中可能没有明确说明,但在这种情况下,因为它是一个容器,我可以兼顾两全。在push_back时,我可以将元素视为未初始化,在pop_back时,我可以将元素视为已初始化。所以所有的 [0, size()) 都已经初始化了,[size(), KCount) 是未初始化的。 - Travis Gockel
@Travis,我现在明白了。对于非类类型,不需要这个标志!好的,让我们来修复它。 - Johannes Schaub - litb
@Travis 那么std::vector似乎是正确的方法。使用resize,然后再使用push_back,就会调用构造函数。 - Johannes Schaub - litb

3
你可以创建一个 char 数组,然后在需要时使用定位new来创建元素。
template <typename T, size_t KCount>
class Array
{
private:
    char m_buffer[KCount*sizeof(T)]; // TODO make sure it's aligned correctly

    T operator[](int i) {
        return reinterpret_cast<T&>(m_buffer[i*sizeof(T)]);
    }

在重新阅读您的问题后,似乎您需要一个稀疏数组,有时也被称为 映射 ;o) (当然性能特征不同...)

template <typename T, size_t KCount>
class SparseArray {
    std::map<size_t, T> m_map;
public:
    T& operator[](size_t i) {
        if (i > KCount)
            throw "out of bounds";
        return m_map[i];
    }

1
我在考虑使用reinterpret_cast,因为从语义上讲,这更有意义。但我真的希望避免这种情况——有没有不得不这样做的方法? - Travis Gockel
你是对的,static_cast 是错误的选择,我会更新我的回答。 - Motti
你的第一个解决方案就是我正在寻找的,基本上正是我想要的。 - Travis Gockel
1
关于这个问题,我有一些注释:你需要将reinterpret_cast转换为T&而不是T。@Travis,请注意您必须使用此代码进行适当的对齐。很可能在没有任何其他工作的情况下,“Array<double,N> a;”未正确对齐,并且在某些平台上会随机崩溃(例如,在sun sparc上)。还要注意,您需要相应的标志(这本质上就是我在解决方案中所做的)。否则,您无法决定何时进行“new”操作和何时不进行。我怀疑代码被省略了以简洁为主。无论如何,如果您喜欢它,尽情享受吧。毕竟这是您的代码 :) - Johannes Schaub - litb
我实际上使用return reinterpret_cast<T*>(m_buffer)[i],因为它看起来更简洁。但是我忘记了正确的对齐方式 - 很好地捕捉到了。 - Travis Gockel
@Johannes,你像往常一样是对的(两个方面都是),关于对齐,我总是认为放置new是用于对齐到所有类型的new内存上的,所以我在这里错过了它。 - Motti

1

这段代码:

#include <iostream>
#include <vector>
using namespace std;

int created = 0, destroyed = 0;

struct S
{
    S()
    {
        ++created;
    }
    S(const S & s ) {
        ++created;
    }
    ~S()
    {
        ++destroyed;
    }
};

int main()
{
    {
        std::vector<S> vec;
        vec.reserve(50);
    }

    std::cout << "Created:\t"   << created   << std::endl;
    std::cout << "Destroyed:\t" << destroyed << std::endl;
    return 0;
}

具有你想要的精确输出 - 我不确定你的问题是什么。


他之后不能使用vec[1]。(惰性构建) - Johannes Schaub - litb
这是正确的(正如我的问题所述),但是std::vector如何做到这一点?通过查看g++的实现,它似乎在char[]上执行了许多reinterpret_cast以避免初始化直到最后一秒钟。 - Travis Gockel
@Travis向量创建未初始化的内存,然后使用放置new在需要时创建实际对象。 - anon

1
如果你想像向量一样,你应该这样做:
template <typename T>
class my_vector
{
    T* ptr; // this is your "array"
    // ...
    public:

    void reserve(size_t n)
    {
        // allocate memory without initializing, you could as well use malloc() here
        ptr = ::operator new (n*sizeof(T)); 
    }

    ~my_vector()
    {
        ::operator delete(ptr); // and this is how you free your memory
    }

    void set_element(size_t at, const T& element = T())
    {
        // initializes single element
        new (&ptr[at]) T(element); // placement new, copies the passed element at the specified position in the array
    }

    void destroy_element(size_t at)
    {
        ptr[at].~T(); // explicitly call destructor
    }
};

这段代码仅用于演示,我省略了my_vector的复制构造函数以及对创建和未创建对象进行跟踪(在未调用构造函数的位置上调用析构函数可能是未定义的行为)。此外,STL的vector分配和释放通过使用分配器(vector的第二个模板参数)进行抽象。

希望这有所帮助。


0

你可以看看STL容器的实现方式,但我怀疑你无法避免使用mallocfree


我可以通过使用char m_buffer[KCount * sizeof(T)]来避免使用mallocfree(尽管显然具有不同的语义,因为这是不允许的)。 - Travis Gockel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接