是否可以在 C90 中模拟 C99 左值数组初始化?

Is it possible to simulate C99 lvalue array initialization in C90?

上下文:

我正在试验 C90 中的函数式编程模式。

目标:

这就是我要在 ISO C90 中实现的目标:

struct mut_arr tmp = {0};
/* ... */
struct arr const res_c99 = {tmp};

正在用左值 (tmp) 初始化 struct mut_arr 类型的 const struct 成员。

#include <stdio.h>

enum
{
    MUT_ARR_LEN = 4UL
};

struct mut_arr
{
    unsigned char bytes[sizeof(unsigned char const) * MUT_ARR_LEN];
};

struct arr {
    struct mut_arr const byte_arr;
};

static struct arr map(struct arr const* const a,
               unsigned char (*const op)(unsigned char const))
{
    struct mut_arr tmp = {0};
    size_t i = 0UL;

    for (; i < sizeof(tmp.bytes); ++i) {
        tmp.bytes[i] = op(a->byte_arr.bytes[i]);
    }

    
    struct arr const res_c99 = {tmp};
    return res_c99;
}

static unsigned char op_add_one(unsigned char const el)
{
    return el + 1;
}

static unsigned char op_print(unsigned char const el)
{
    printf("%u", el);
    return 0U;
}

int main() {
    struct arr const a1 = {{{1, 2, 3, 4}}};

    struct arr const a2 = map(&a1, &op_add_one);

    map(&a2, &op_print);

    return 0;
}

这是我在 C90 中尝试过的:

#include <stdio.h>
#include <string.h>

enum {
    MUT_ARR_LEN = 4UL
};

struct mut_arr {
    unsigned char bytes[sizeof(unsigned char const) * MUT_ARR_LEN];
};

struct arr {
    struct mut_arr const byte_arr;
};

struct arr map(struct arr const* const a,
               unsigned char (*const op)(unsigned char const))
{
    struct arr const res = {0};
    unsigned char(*const res_mut_view)[sizeof(res.byte_arr.bytes)] =
        (unsigned char(*const)[sizeof(res.byte_arr.bytes)]) & res;

    struct mut_arr tmp = {0};
    size_t i = 0UL;

    for (; i < sizeof(tmp.bytes); ++i) {
        tmp.bytes[i] = op(a->byte_arr.bytes[i]);
    }

    memcpy(res_mut_view, &tmp.bytes[0], sizeof(tmp.bytes));
    return res;
}

unsigned char op_add_one(unsigned char const el) { return el + 1; }

unsigned char op_print(unsigned char const el) {
    printf("%u", el);
    return 0U;
}

int main() {
    struct arr const a1 = {{{1, 2, 3, 4}}};

    struct arr const a2 = map(&a1, &op_add_one);

    map(&a2, &op_print);

    return 0;
}

我所做的就是创建一个“备用视图”(使其基本上可写)。因此,我将返回的地址转换为 unsigned char(*const)[sizeof(res.byte_arr.bytes)]。 然后,我使用memcpy,并将tmp的内容复制到res

一开始我也尝试过使用作用域机制来绕过初始化。 但这无济于事,因为无法进行运行时评估。

这可行,但与上面的 C99 解决方案完全不同。 是否有更优雅的方法来实现这一目标?

PS:解决方案最好也应该尽可能便携。 (没有堆分配,只有静态分配。它应该保持线程安全。上面的这些程序似乎是,因为我只使用堆栈分配。)

联合起来。

#include <stdio.h>
#include <string.h>

enum {
    MUT_ARR_LEN = 4UL
};

struct mut_arr {
    unsigned char bytes[sizeof(unsigned char) * MUT_ARR_LEN];
};

struct arr {
    const struct mut_arr byte_arr;
};

struct arr map(const struct arr *a, unsigned char (*op)(unsigned char)) {
    union {
        struct mut_arr tmp;
        struct arr arr;
    } u;
    size_t i = 0;
    for (; i < sizeof(u.tmp.bytes); ++i) {
        u.tmp.bytes[i] = op(a->byte_arr.bytes[i]);
    }
    return u.arr;
}

unsigned char op_add_one(unsigned char el) {
    return el + 1;
}

unsigned char op_print(unsigned char el) {
    printf("%u", el);
    return 0U;
}

int main() {
    const struct arr a1 = {{{1, 2, 3, 4}}};
    const struct arr a2 = map(&a1, &op_add_one);

    map(&a2, &op_print);

    return 0;
}

让我们抛出一些来自 https://port70.net/~nsz/c/c89/c89-draft.html 的标准内容。

One special guarantee is made in order to simplify the use of unions: If a union contains several structures that share a common initial sequence, and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them. Two structures share a common initial sequence if corresponding members have compatible types for a sequence of one or more initial members.

Two types have compatible type if their types are the same.

For two qualified types to be compatible, both shall have the identically qualified version of a compatible type;

想法是 mut_arrarr 的“公共初始序列”是 unsigned char [sizeof(unsigned char) * MUT_ARR_LEN]; 因此您可以使用另一个访问一个。

但是,正如我现在阅读的那样,未指定“如果对应成员的初始序列”是否包含嵌套结构成员。所以从技术上讲要符合超级标准,你会:

struct arr map(const struct arr *a, unsigned char (*op)(unsigned char)) {
    struct mutmut_arr {
       struct mut_arr byte_arr;
    };
    union {
        struct mutmut_arr tmp;
        struct arr arr;
    } u;
    size_t i = 0;
    for (; i < sizeof(u.tmp.bytes); ++i) {
        u.tmp.byte_arr.bytes[i] = op(a->byte_arr.bytes[i]);
    }
    return u.arr;
}

@subjective 我想说明两点。

const 类型限定符在代码中的位置非常混乱。在 C 中,通常写 const <type> 而不是 <type> const。通常 * 向右对齐,space 向左对齐。我根本无法有效地阅读您的代码。我从上面的代码中删除了几乎所有 const

创建这样的界面将是痛苦的,并没有太大的好处,许多边缘情况下潜伏着未定义的行为。在 C 编程语言中,相信程序员 - 它是 the principles of C programming language 之一。不要阻止程序员做必须做的事情(初始化结构成员)。我建议让成员可变并有一个结构定义并将其称为日。 const 合格的结构体成员一般都比较难对付,并没有什么大的好处。

我的回答乍一看可能很离谱。是

立即停止手头的工作!

我会花时间解释并让你瞥见你的未来(如果你追求这个想法,那是暗淡的)并试图说服你。但我回答的要点是上面的粗线。

  1. 您的原型省略了关键部分,以便为您的“C 函数式编程”方法提供一些持久的解决方案。例如,您只有字节数组 (unsigned char)。但是对于“真实”程序员的“真实”解决方案,您需要考虑不同的类型。如果你转到 hoogle (Haskells online type and function browser engine thingy),你会注意到,你试图在 C 中实现的功能特性 fmap 定义为:
    fmap :: Functor f => (a -> b) -> f a -> f b
    这意味着,映射并不总是从类型 a 到类型 a。这是一个 monadic thingy,您尝试提供您的 C 编程人员。因此,类型元素类型a的数组需要映射到元素类型b的数组。因此,您的解决方案需要提供的不仅仅是字节数组。

  2. 在C语言中,数组可以驻留在不同类型的内存中,我们不能很好地隐藏这一点。 (在真正的函数式语言中,内存管理在很大程度上是抽象的,你根本不关心。但在 C 中,你 必须 关心。你的库的用户必须关心和你需要让他们尽职尽责。数组可以是全局的,在堆栈上,在堆上,在共享内存中......你需要提供一个解决方案,允许所有这些。否则,它永远只是一个玩具,传播一种错觉,即“这是可能的和有用的”。

因此,只允许不同的自定义类型的数组(请注意,有人也会想要一种类型的数组的数组!)并了解内存管理,下一个演变的头文件怎么可能看起来像。这是我想出的:

#ifndef __IMMUTABLE_ARRAY_H
#define __IMMUTABLE_ARRAY_H

#include <stdint.h>
#include <stdlib.h>
#include <stdatomic.h>

// lacking namespaces or similar facilities in C, we use
// the prefix IA (Immutable Array) in front of all the stuff
// declared in this header.

// Wherever you see a naked `int`, think "bool".
// 0 -> false, 1 -> true.
// We do not like stdbool.h because sometimes trouble
// ensues in mixed C/C++ code bases on some targets, where
// sizeof(C-bool) != sizeof(C++-bool) o.O. So we cannot use
// C-bool in headers...

// We need storage classes!
// There are arrays on heap, static (global arrays),
// automatic arrays (on stack, maybe by using alloca),
// arrays in shared memory, ....
// For those different locations, we need to be able to
// perform different actions, e.g. for cleanup.
// IAStorageClass_t defines the behavior for a specific
// storage class.
// There is also the case of an array of arrays to consider...
// where we would need to clean up each member of the array
// once the array goes out of scope.

struct IAArray_tag;

typedef struct IAArray_tag IAArray_t;

typedef struct IAStorageClass_tag IAStorageClass_t;

typedef int (*IAArrayAllocator) (IAStorageClass_t* sclass,
                 size_t elementSize,
                 size_t capacity,
                 void* maybeStorage,
                 IAArray_t* target);

typedef void (*IAArrayDeleter) (IAArray_t* arr);
typedef void (*IAArrayElementDeleter) (IAArray_t* arr);
typedef int64_t (*IAArrayAddRef) (IAArray_t* arr);
typedef int64_t (*IAArrayRelease) (IAArray_t* arr);

typedef struct IAStorageClass_tag {
  IAArrayAllocator allocator;
  IAArrayDeleter deleter;
  IAArrayElementDeleter elementDeleter;
  IAArrayAddRef addReffer;
  IAArrayRelease releaser;
} IAStorageClass_t;

enum IAStorageClassID_tag {
  IA_HEAP_ARRAY = 0,
  IA_STACK_ARRAY = 1,
  IA_GLOBAL_ARRAY = 2,
  IA_CUSTOM_CLASSES_BEGIN = 100
};

typedef enum IAStorageClassID_tag IAStorageClassID_t;

// creates the default storage classes (for heap and automatic).
void IAInitialize();
void IATerminate();

// returns a custom and dedicated identifier of the storage class.
int32_t
IARegisterStorageClass
(IAArrayAllocator allocator,
 IAArrayDeleter deleter,
 IAArrayElementDeleter elementDeleter,
 IAArrayAddRef addReffer,
 IAArrayRelease releaser);

struct IAArray_tag {
  const IAStorageClass_t* storageClass;
  int64_t refCount;
  size_t elementSize; // Depends on the type you want to store
  size_t capacity;
  size_t length;
  void* data;
};

// to make sure, uninitialized array variables are properly
// initialized to a harmless state.
IAArray_t IAInitInstance();

// allows to check if we ran into some uninitialized instance.
// In C++, this would be like after default constructor.
// See IAInitInstance().
int IAIsArray(IAArray_t* arr);

int
IAArrayCreate
(int32_t storageClassID,
 size_t elementSize,     // the elementSize SHALL be padded to
                         // a system-acceptable alignment size.
 size_t capacity,
 size_t size,
 void* maybeStorage,
 IAArray_t* target);

typedef
int
(*IAInitializerWithIndex_t)
(size_t index,
 void* elementPtr);

int
IAArrayCreateWithInitializer
(int32_t storageClassID,
 size_t elementSize,
 size_t capacity,
 void* maybeStorage,
 IAInitializerWithIndex_t initializer,
 IAArray_t* target);

IAArray_t*  IAArrayAddReference(IAArray_t* arr);
void IAArrayReleaseReference(IAArray_t* arr);

// The one and only legal way to access elements within the array.
// Shortcutters, clever guys and other violators get hung, drawn
// and quartered!
const void * const IAArrayAccess(IAArray_t* arr, size_t index);

typedef void (*IAValueMapping_t)
(size_t index,
 void* sourceElementPtr,
 size_t sourceElementSize,
 void* targetElementPtr,
 size_t targetElementSize);

size_t IAArraySize(IAArray_t* arr);
size_t IAArrayCapacity(IAArray_t* arr);
size_t IAArrayElementSize(IAArray_t* arr);

// Because of reasons, we sometimes want to recycle
// an array and populate it with new values.
// This can only be referentially transparent and safe,
// if there are no other references to this array stored
// anywhere. i.e. if refcount == 1.
// If our app code passed the array around to other functions,
// some nasty ones might sneakily store themselves a pointer
// to an array and then the refcount > 1 and we cannot
// safely recycle the array instance.
// Then, we have to release it and create ourselves a new one.
int IACanRecycleArray(IAArray_t* arr);


// Starship troopers reporter during human invasion
// of bug homeworld: "It is an ugly planet, a bug planet!"
// This is how we feel about C. Map needs some noisy extras,
// just because C does not allow to build new abstractions with
// types. Yes, we could send Erich Gamma our regards and pack
// all the noise into some IAArrayFactory * :) 
int
IAArrayMap(IAValueMapping_t mapping,
       IAArray_t* source,
       int32_t targetStorageClassID,
       size_t targetElementSize,
       void* maybeTargetStorage,
       IAArray_t* target);


#endif

不用说,我懒得在我仍然空虚的immutable-array.c中实现我的可爱immutable-array.h,是吗?

但是一旦我们做到了,快乐就会开始,我们可以编写健壮的、函数式的 C 程序,是吗?不!使用这些数组编写的函数式 C 应用程序代码可能如下所示:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <stdatomic.h>
#include <math.h>
#include <assert.h>

#include "immutable-array.h"

typedef struct F64FloorResult_tag {
  double div;
  double rem;  
} F64FloorResult_t;

void myFloor(double number, F64FloorResult_t* result) {
  if (NULL != result) {
    result->div = floor(number);
    result->rem = number - result->div;
  }
}

int randomDoubleInitializer(size_t index, double* element) {
  if (NULL != element) {
    *element = ((double)rand()) / (double)RAND_MAX;
    return 1;
  }
  return 0;
}

void
doubleToF64FloorMapping
(size_t index,
 double* input,
 size_t inputElementSize,
 F64FloorResult_t *output,
 size_t outputElementSize) {
  assert(sizeof(double) == inputElementSize);
  assert(sizeof(F64FloorResult_t) == outputElementSize);
  assert(NULL != input);
  assert(NULL != output);
  myFloor(*input, output);
}

int main(int argc, const char* argv[]) {
  IAInitialize();
  {
    double sourceData[20];
    IAArray_t source = IAInitInstance();
    if (IAArrayCreateWithInitializer
    ((IAStorageClassID_t)IA_STACK_ARRAY,
     sizeof(double),
     20,
     &sourceData[0],
     (IAInitializerWithIndex_t)randomDoubleInitializer,
     &source)) {
      IAArray_t result = IAInitInstance();
      F64FloorResult_t resultData[20];
      if (IAArrayMap
      ((IAValueMapping_t)doubleToF64FloorMapping,
       &source,
       (int32_t)IA_STACK_ARRAY,
       sizeof(F64FloorResult_t),
       &result)) {
    assert(IAArraySize(&source) == IAArraySize(&result));
    for (size_t index = 0;
         index < IAArraySize(&source);
         index++) {
      const double* const ival =
        (const double* const)IAArrayAccess(&source, index);
      const F64FloorResult_t* const oval =
        (const F64FloorResult_t* const)
        IAArrayAccess(&result,index);
      printf("(%g . #S(f64floorresult_t :div %g :rem %g))\n",
         *ival, oval->div, oval->rem);
    }
    IAArrayReleaseReference(&result);
      }
      IAArrayReleaseReference(&source);
    }
  }
  IATerminate();
  return 0;
}

如果你试图将这样的怪物强加给他们,我已经看到你的同事的书包里有刀子了。他们会恨你,你会恨你自己。最终,你会讨厌你曾经有过尝试的想法。

特别是,如果使用更合适的语言,相同的代码可能如下所示:

(map 'list #'(lambda (x) (multiple-value-list (floor x)))
          (loop repeat 20
            for x = (random 1.0)
            collecting x))