使用宏在运行时堆栈中优雅地创建一个可变大小的“二维指针”列表

Elegantly create a variable-size "2D-pointer" list in runtime-stack, using macros

这在某种程度上既是最佳实践问题,也是技术问题,并且有一些上下文。

TL-DR:我想使用单行定义宏来创建匿名可变大小数组。

请注意,在我下面的问题中,我不能使用 gcc 扩展 - 我只能将香草 C99 视为理所当然。

上下文

在我的项目(外部用户使用的模拟软件)中,有几个二维数组的结构

typedef struct ComplexMatrix2
{
    double real[2][2];
    double imag[2][2];
} ComplexMatrix2;

typedef struct ComplexMatrix4
{
    double real[4][4];
    double imag[4][4];
} ComplexMatrix4;

typedef struct ComplexMatrixN 
{
    int n;
    double** real;
    double** imag;
} ComplexMatrixN;

如您所见,ComplexMatrix 24 具有固定大小的二维数组,而 ComplexMatrixN 是可变大小的,因此它的 real imag 字段是指针。这是由于 ComplexMatrix 24 更常用,因此需要 efficiently/concisely 创建。下面是一些示例用法:

// passing compound literal in-line, kept in stack
myfunc2( (ComplexMatrix2) {.real={{1,2},{3,4}}, .imag={{0}}} );
// initialising in stack
ComplexMatrix4 m4 = {
    .real = {{1,2,3,4},
             {5,6,7,8},
             {9,10,11,12},
             {13,14,15,16}},
    .imag = {{0}}};
myfunc4( m );
// no initialisation, not in-line, created in heap
ComplexMatrixN m8 = createComplexMatrixN(8);

for (int i=0; i<8; i++)
    for (int j=0; j<8; j++)
        m16.real[i][j] = 1;

myfuncN(m8);

destroyComplexMatrixN(m8);

非常不幸的是,尽管很少使用,ComplexMatrixN 必须动态创建,不能初始化并且不能 created/passed 内联。

请注意,用户极不可能制作大 ComplexMatrixN,例如大于 numQubits=6。因此,将 ComplexMatrixN 保留在堆栈中是安全的(如果可能),并且可以限制这样做的能力。

任务

我希望用户能够根据需要在堆栈中优雅地创建 ComplexMatrixN。也就是说,.real.imag 字段是堆栈中的数组,尽管它们解析为 double**.

这是一种方法:

// internal code
ComplexMatrixN bindArraysToStackComplexMatrixN(
    int n, double re[][n], double im[][n], 
    double** reStorage, double** imStorage)
{
    ComplexMatrixN m;
    m.n = n;
    m.real = reStorage;
    m.imag = imStorage;

    for (int i=0; i<n; i++) {
        m.real[i] = re[i];
        m.imag[i] = im[i];
    }
    return m;
}
// user code
ComplexMatrixN m2 = bindArraysToStackComplexMatrixN(
    2, 
    // initialising values
    (double[][2]) {{1,2},{3,4}}, 
    (double[2][2]) {{0}},
    // stack-space for array of pointers
    (double*[2]) {}, 
    (double*[2]) {}
);

myfuncN(m2);

main 中的调用代码在堆栈中分配 4 个数组,bindArraysToStackComplexMatrixN 只是填充它们并将它们绑定到 ComplexMatrixN 实例。 对 bindArraysToStackComplexMatrixN 的调用甚至可以在 myfuncN 的参数内进行,这样 ComplexMatrixN 就不会在范围内!

尽管此方法允许初始化、堆栈中创建和内联创建,但它有两个缺点:

所以我想知道,对于 变量 大小 ComplexMatrixN,我是否可以使用宏来自动提供 (double*[4]) {}, (double*[4]) {}。这对用户来说是一个安全的宏,因为它最好是单行的,并且不会向调用范围添加任何新变量。

一次尝试

根据 this question.

,对于 变量 大小,最明显的方法是不可能的

即有人可能认为我们可以简单地定义一个宏

// internal code
#define createStackComplexMatrixN(num, re, im) ( \
    bindArraysToStackComplexMatrixN( \
        num, re, im, \
        (double*[num]) {}, (double*[num]) {} \
    ) \
)
// user code
ComplexMatrixN m = createStackComplexMatrixN(
    1,
    (double[][2]) ({{1,2},{3,4}}), 
    (double[2][2]) ({{0}})
);
myfuncN(m);

// note user had to wrap array literals in brackets, else macro misintreprets #args

但这将调用 error: compound literal has variable size,因为 (double[num]) {} 在 C99 中是非法的。

绕过此限制的下一个明显方法是不尝试初始化指针数组:

// internal code
#define createStackComplexMatrixN(num, re, im) ( { \
    double* reStorage_[num]; \
    double* imStorage_[num]; \
    bindArraysToStackComplexMatrixN( \
        num, re, im, reStorage_, imStorage_) \
} )
// user code
ComplexMatrixN m = createStackComplexMatrixN(
    1,
    (double[][2]) ({{1,2},{3,4}}),
    (double[2][2]) ({{0}})
);
myfuncN(m);

在此示例中有效,但现在...

myfuncN(
    createStackComplexMatrixN(1,
        (double[][2]) ({{1,2},{3,4}}),
        (double[2][2]) ({{0}}))
);

现在无效(请注意我不能使用 Blocks extension,这将解决后一个问题)。

一个不优雅的解决方案

我们可以通过为用户可能想要创建的每种大小定义许多宏来绕过可变大小的复合文字。

#define getCompMatr6(re, im) ( \
    bindArraysToStackComplexMatrixN( \
        6, (double[6][6]) re, (double[6][6]) im, \
        (double*[6]) {}, (double*[6]) {} \
    ) \
)

#define getCompMatr8(re, im) ( \
    bindArraysToStackComplexMatrixN( \
        8, (double[8][8]) re, (double[8][8]) im, \
        (double*[8]) {}, (double*[8) {} \
    ) \
)

...
// user code

myfuncN(
    getCompMatr6(
        ({{1,2,3,4,5,6}, 
         {9,8,7,6,5,4}, {0}}),
        ({{0}})));

myfuncN(
    getCompMatr8(
        ({{1,2,3,4,5,6,7,8}, {0}}),
        ({{0}})));

这对最终用户来说相对优雅。然而,在这个例子中,他们将不得不调用不同的函数来初始化不同大小的ComplexMatrixN(尽管这并不是用户真正想要自动执行的事情,所以没关系)。

稍微优雅一点的解决方案

我们可以允许用户改为传递 ComplexMatrixN 的文字大小,以避免任何代码重复,并避免使命名空间混乱!

// internal code
#define getCompMatrN(num, re, im) ( \
    bindArraysToStackComplexMatrixN( \
        num, (double[num][num]) re, (double[num][num]) im, \
        (double*[num]) {}, (double*[um]) {} \
    ) \
)
// user code
myfuncN(
    getCompMatrN(
        8,
        ({{1,2,3,4,5,6,7,8}, 
         {9,8,7,6,5,4,3,2}, {0}}),
        ({{0}})));

myfuncN(
    getCompMatrN(
        16,
        ({{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}, {0}}),
        ({{0}})));

用户这样做是无效的

int var = 2;
getCompMatrN(var, ...)

因为这将调用可变长度复合错误。

但是,此解决方案似乎不起作用!它产生编译错误:

warning: expression result unused [-Wunused-value]
    ComplexMatrixN m = getCompMatrN(2, ({{1,2},{3,4}}), {{0}});
                                                    ^
note: expanded from macro 'getCompMatrN'
        num, (double[num][num]) re, (double[num][num]) im, \
                                     ^
error: expected ';' after expression
    ComplexMatrixN m = getCompMatrN(2, ({{1,2},{3,4}}), {{0}});
                                                       ^
note: expanded from macro 'getCompMatrN'
        num, (double[num][num]) re, (double[num][num]) im, \
                                     ^
warning: expression result unused [-Wunused-value]
    ComplexMatrixN m = getCompMatrN(2, ({{1,2},{3,4}}), {{0}});
                                                      ^
note: expanded from macro 'getCompMatrN'
        num, (double[num][num]) re, (double[num][num]) im, \
                                     ^
error: expected expression
    ComplexMatrixN m = getCompMatrN(2, ({{1,2},{3,4}}), {{0}});
                                                        ^
note: expanded from macro 'getCompMatrN'
        num, (double[num][num]) re, (double[num][num]) im, \
                                     ^
error: used type 'double [2][2]' where arithmetic or pointer type is
      required
    ComplexMatrixN m = getCompMatrN(2, ({{1,2},{3,4}}), {{0}});
                       ^                         ~~~~~~~~~~~~~~~
note: expanded from macro 'getCompMatrN'
        num, (double[num][num]) re, (double[num][num]) im, \
             ^
error: expected expression
note: expanded from macro 'getCompMatrN'
    ); \
       ^

诚然,我不明白这些错误的原因。

我的问题

您上一个宏定义中的错误是复合文字具有以下语法 (§6.5.2):

( type-name ) { initializer-list }
( type-name ) { initializer-list , }

所以 (double[2][2])({{1,2},{3,4}}) 不是复合文字。 (double[2][2]){{1,2},{3,4}} 会,但是你需要玩点小把戏来将该初始化程序作为宏参数提供,因为没有括号,逗号将不会受到保护。这是一种方法:

/* This macro is used to strip parentheses from an argument */
#define ID(...) __VA_ARGS__

#define getCompMatrN(num, re, im) ( \
    bindArraysToStackComplexMatrixN( \
        num, (double[num][num]) ID re, (double[num][num]) ID im, \
        (double*[num]) {NULL}, (double*[num]) {NULL} \
    ) \
)

ComplexMatrixN m = 
    getCompMatrN(2, ({{1,2},{3,4}}), ({{0}}));

请注意,对于此公式,初始化列表周围的括号是 必需的(请参阅最后一行 getCompMatrN 的第三个参数的更改)。

我将 bindArraysToStackComplexMatrixN 的最后两个参数更改为 (double*[num]) {NULL} 因为空的初始化列表不是有效的 C,即使某些编译器可能会接受它们。

我从这个相当冗长的问题中收集到的要求是

  • objective是为struct ComplexMatrixN类型对象的初始化器提供一个宏。
  • 所涉及的各种指针所指向的数组的维数必须被接受为一个或多个宏参数。
  • 该解决方案应至少提供一个完全基于堆栈的值的选项。
  • 只能使用标准 C99 的功能。

虽然问题中没有明确表达,但我认为还需要以下特征:

  • 可以在任何给定范围内安全地多次使用初始化宏。
  • None 个生成的初始化值互为别名。

另外,这个问题似乎说的是:

  • 结果值在初始值设定项出现的上下文之外仍然有效和可用。

而且我认为

也是可取的
  • 该宏可在文件范围内使用。

不幸的是,struct ComplexMatrixN 具有指针成员这一事实为同时实现所有这些提供了一些不可逾越的障碍。这些成员需要用指向合适类型对象的有效指针值初始化,并且如果指向的对象是在某个块范围内声明的,而不是在文件范围内,那么当控制权移出块时,它们的生命周期结束.这适用于复合文字表示的对象,就像它适用于标量类型的对象一样,所以

  1. 如果对象是用指向堆栈分配对象的指针初始化的,那么它们的值将不能在初始化程序出现的块之外安全地使用。根据详细信息,它们甚至可能无法在该块内安全使用。

如果要涉及复合文字,那么了解

也很重要
  1. 复合文字的语法不允许将初始化部分括在括号中。这是问题末尾出现的错误的主要来源。尽可能清楚:

    // This contains a compound literal:
    double (*dp)[2] = (double[2][2]) {{1,2},{3,4}};
    
    // whereas THIS IS WRONG:
    double (*dp2)[2] = (double[2][2]) ({{1,2},{3,4}});
    

这意味着通常你不能将初始化部分作为单个宏参数单独传递,因为没有括号,其中的逗号将被视为分隔宏参数。这对所有

的人来说都是一个严重的问题

我还注意到

  1. 任何涉及以任何方式调用函数或涉及初始化程序之外的赋值代码的解决方案都与在文件范围内的使用不兼容。 (虽然文件范围的东西是我自己添加的,所以可能有点像稻草人。)

如何最好地进行取决于您需要解决方案的通用性、您希望它提供多少帮助以及您愿意使用多少宏观魔法。


一个替代方案几乎是问题中提出的第一次尝试。您必须更正不允许使用空初始值设定项的问题,才能生成此宏:

#define createStackComplexMatrixN(num, re, im) \
    bindArraysToStackComplexMatrixN( \
        num, re, im, \
        (double*[num]) {0}, (double*[num]) {0} \
    )

(我还删除了外括号。)关键是正确调用它。特别是,如果 reim 参数表示为复合文字,则它们必须(全部)都用括号括起来:

ComplexMatrixN m = createStackComplexMatrixN(
    2,
    ((double[][2]) {{1,2},{3,4}}),
    ((double[2][2]) {{0}})
);

当然,正如已经讨论过的,它不能出现在文件范围内,它只对通过复合文字表示的结构的生命周期有用。


可以想象,您可以设计一个基于可变参数宏和宏伪迭代的解决方案,这对用户来说更容易编写,或者至少不太容易出现不一致,但这更像是一个项目比我准备承担的 SO 答案。