C++模板:避免占用额外空间的零长度数组

4

注意:

根据liveworkspace.org的说法,这个问题的答案适用于最新版本的g++(4.7.2),clang(3.2)和icc(13.0.1),但是根据Stephen Lin的评论,它取决于空基类优化std::tuple的实现。


原始问题:

如果我有一个模板结构体像这样:

template<
    class T1, unsigned short N1,
    class T2, unsigned short N2
>
struct ComboThree {
    T1 data_1[N1];
    T2 data_2[N2];
};

我可以通过专门化来避免零长度数组和额外的对齐填充:

template<class T1, class T2>
struct ComboThree<T1, 0, T2, 0> {
};

template<class T1, class T2, unsigned short N2>
struct ComboThree<T1, 0, T2, N2>
{
    T2 data_2[N2];
};

template<class T1, unsigned short N1, class T2>
struct ComboThree<T1, N1, T2, 0>
{
    T1 data_1[N1];
};

当TX/NX对中的X变得更大时,像这样专门化变得有些麻烦。在我的项目中,实际的不同组合数量可能会少于五个,因此我最终可能根本不使用模板,但我很好奇:

是否有一种使用模板魔法来避免零长度数组同时又不占用任何额外空间的方法?

例如,这样做:

template<class T, unsigned short N>
struct Array {
    T data[N];
};

template<class T>
struct Array<T, 0> {};

template<
    class T1, unsigned short N1,
    class T2, unsigned short N2
>
struct ComboTwo {
    Array<T1, N1> data_1;
    Array<T2, N2> data_2;
};

避免使用长度为零的数组,但空结构体需要占用额外的空间。另一方面,下面这种情况:
template<class T, unsigned short N>
struct Array {
    T data[N];
};

template<class T>
struct Array<T, 0> {};

template<
    class T1, unsigned short N1,
    class T2, unsigned short N2
>
struct ComboFour : Array<T1, N1>, Array<T2, N2> {};

看起来做我想要的事情(是吗?),但我不知道如何在我的程序中访问Array<>基本结构中的数据。 Stephen Lin也指出了它的其他限制。


1
请注意,ComboFourArray<T, 0> 子对象可能需要零空间,这被称为“空基类优化”,并且是标准允许的,但不是必需的。 - Stephen Lin
你可以用100美元购买8GB的RAM。这是解决这个问题的替代方案。 - Bo Persson
@BoPersson 在这种情况下可能是过早的优化,但空子对象浪费的空间确实会累积;否则库编写者不会花费如此多的精力在 EBO 上。 - Stephen Lin
@Stephen - 我在考虑投资回报率。Stack Overflow 上有不少关于节省纳秒或几个字节的问题。我们能负担得起这样做吗? - Bo Persson
@BoPersson - 我正在考虑使用类似这样的东西来存储3D模型中单个顶点的位置/法线等数据,如果这有助于解释我的空间问题。 - tecu
@BoPersson 嗯,你只需要编写一次通用解决方案,就可以永久地获得好处;这基本上是C++的哲学。所以这完全取决于这将被重复使用多少次以及在哪里使用。 - Stephen Lin
1个回答

2

这不是一个完整的答案,但在评论中很难解释清楚。如果你真的想要访问ComboFour的子对象,并采用最后一种选项,需要使用以下丑陋的语法:

ComboFour<int, 2, float, 1> cf;
cf.Array<int, 2>::data[0] = 0;
cf.Array<int, 2>::data[1] = 1;
cf.Array<float, 1>::data[0] = 2.0f;

你可能可以通过一些访问器函数来简化这个过程,但仍然不够好。

然而,更大的问题在于以下内容是错误的:

ComboFour<int, 1, int, 1> cf2 // fails to compile

因为相同的类不能被用作父类两次。

(另外,请注意,ComboFourArray<T, 0> 子对象可能会占用零空间,这称为“空基类优化”,标准允许但不要求。)

也许有某种方法可以解决第二个问题...我认为从 std::tuple<...> 继承(可能由您的标准库实现在内部使用空基类优化)的 Array<T, N> 是最简单的方法,如果您真的必须这样做,但它会使语法更加丑陋。

编辑:在 GCC 4.7.2 上对我有效。

template<
    class T1, unsigned short N1,
    class T2, unsigned short N2,
    class T3, unsigned short N3
>
struct Combo : std::tuple<Array<T1, N1>, Array<T2, N2>, Array<T3, N3>>
{
};

之后...

Combo<int, 2, int, 2, float, 3> c;
std::get<0>(c).data[0] = 0;
std::get<0>(c).data[1] = 1;
std::get<1>(c).data[0] = 2;
std::get<1>(c).data[1] = 3;
std::get<2>(c).data[0] = 0.0;
std::get<2>(c).data[1] = 1.0;
std::get<2>(c).data[2] = 2.0;

assert(sizeof(Combo<int, 0, int, 0, float, 1>) == sizeof(float));

说实话,标准库或多或少需要使用空基类优化才能使用,所以即使不是必需的,如果最近的编译器不支持它,我会感到惊讶;至于std::tuple<...>是否正确编写以充分利用这种优化,则是另一回事。


谢谢。我刚刚注意到“更大的问题”,回来编辑时留意到了这一点。同时也感谢您教给我语法。 - tecu
顺便说一下,你可能可以使用宏来使语法不那么丑陋,但是不要告诉任何人我说过这个(宏是邪恶的:D)。 - Stephen Lin
@tecu,你的编译器能够正常工作吗?显然,语法并不是很好,但你可以在“Combo”上编写一些辅助函数来帮助一下。 - Stephen Lin
是的,这对我来说非常棒(还是个很巧妙的技巧)。从你回答中的最后一段来看,我明白了std::tuple在标准中并没有保证会以这种方式工作? - tecu
@tecu,不是的,这完全取决于库的实现是否想要使用此优化,以及编译器是否支持它。 - Stephen Lin

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