简体   繁体   中英

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

Context:

I am experimenting with functional programming patterns in C90.

Goal:

This is what I'm trying to achieve in ISO C90:

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

Initializing a const struct member of type struct mut_arr with a lvalue ( tmp ).

#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;
}

This is what I tried in 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;
}

All I do is to create an "alternate view" (making it essentially writable). Hence, I cast the returned address to unsigned char(*const)[sizeof(res.byte_arr.bytes)] . Then, I use memcpy , and copy the contents of the tmp to res .

I also tried to use the scoping mechanism to circumvent initializing in the beginning. But it does not help, since there cannot be a runtime evaluation.

This works, but it is not anything like the C99 solution above. Is there perhaps a more elegant way to pull this off?

PS: Preferably, the solution should be as portable as possible, too. (No heap allocations, only static allocations. It should remain thread-safe. These programs above seem to be, as I only use stack allocation.)

Union it.

#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;
}

Let's throw some standard stuffs from 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;

The idea is that "common initial sequence" of mut_arr and arr is unsigned char [sizeof(unsigned char) * MUT_ARR_LEN]; so you can access one using the other.

However, as I read it now, it is unspecified if "initial sequence if corresponding members" includes nested struct members or not. So technically to be super standard compliant, you would:

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 I do want to note two things.

The placement of const type qualifier in your code is very confusing. It's typical in C to write const <type> not <type> const . It's typical to align * to the right with space on the left. I was not able to read your code efficiently at all. I removed almost all const from the code above.

Creating such interface as presented will be pain with no great benefits, with a lot of edge cases with lurking undefined behaviors around the corner. In C programming language, trust the programmer - it's one of the principles of C programming language . Do not prevent the programmer to do what has to be done (initializing a structure member). I would advise making the member mutable and have one structure definition and call it day. const qualified structure members usually are just hard to deal with, with no big benefits.

If I understand correctly, you want to compile the first snippet with C89/C90, isn't it?

In such case remember that the first member of a struct and the struct itself shares the same memory address, you just need to dereference a pointer to the original type via cast:

Switch from:

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

to

return *(struct arr *)&tmp;

My answer might sound outrageous at first glance. It is

STOP WHAT YOU ARE DOING, NOW!

I will take my time to explain and give you a glimpse into your future (which is dim, if you pursue this idea) and try to convince you. But the gist of my answer is the bold line above.

  1. Your prototype omits crucial parts to have some lasting solution to your "functional programming in C" approach. For example, you only have arrays of bytes ( unsigned char ). But for a "real" solution for "real" programmers, you need to consider different types. If you go to hoogle (Haskells online type and function browser engine thingy) , you will notice, that fmap , which is the functional feature you try to achieve in C is defined as:
    fmap:: Functor f => (a -> b) -> fa -> fb
    This means, the mapping is not always from type a to type a . It's a monadic thingy, you try to offer your C programming fellows. So, an array of type element type a needs to be mapped to an array of element type b . Hence, your solution needs to offer not just arrays of bytes.

  2. In C, arrays can reside in different types of memory and we cannot hide this very well. (In real functional languages, memory management is kind of abstracted away for the larger part and you just do not care. But in C, you must care. The user of your library must care and you need to allow them to dutifully care. Arrays can be global, on the stack, on the heap, in shared memory, ... and you need to offer a solution, allowing all that. Else, it will always just be a toy, propagating an illusion, that "it is possible and useful".

So, with just allowing arrays of different, custom types (someone will want arrays of arrays of a type as well, mind you,) and to be aware of memory management. how could a header file of your next evolution look like: Here is what I came up with:

#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

Needless to say, that I did not bother to implement my cute immutable-array.h in my still empty immutable-array.c , yes?

But once we did it, the joy woulds begin and we could write robust, functional C programs, yes? No: This is how well written functional C application code using those arrays might look like:

#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;
}

I see already the knives coming out of the satchels of your colleagues if you try to impose such a monstrosity upon them. They will hate you, you will hate yourself. Eventually, you will hate that you ever had the idea to even try.

Especially, if in a more suitable language, the same code might look like this:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM