TJM
TJM

Reputation: 739

How to initialize a static array within a c-style struct by an existing static array?

I'm currently doing c++ with OpenCL, where a c-style struct is required to carry configuration information from the c++ host to the OpenCL kernel. Given that dynamically allocated arrays are not guaranteed to be supported by every OpenCL implementation, I must ensure every array accessible by the kernel code be static-sized. However, I run into weird errors when initializing static arrays within a c-style struct.

The error could be reproduced by the following PoC:

#include <cstring>
#include <string>
#define ID_SIZE 16

struct conf_t {
    const unsigned int a;
    const unsigned int b;
    const unsigned char id[ID_SIZE];
};

int main() {
    const std::string raw_id("0123456789ABCDEF");
    unsigned char id[ID_SIZE];
    memcpy(id,raw_id.c_str(),ID_SIZE);
    struct conf_t conf = {10,2048,id};
}

And the following error:

poc.cc: In function ‘int main()’:
poc.cc:15:39: error: array must be initialized with a brace-enclosed initializer
   15 |         struct conf_t conf = {10,2048,id};
      |                                       ^~

It's true that I could remove the const keyword in the struct and get rid of the stack variable id, where &(conf.id) could be the first parameter of memcpy. However, I'd like to keep the immutability of fields in the conf struct, which enables the compilers to check undesired modifications.

For my understanding, structs in c should have the following memory layout:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               a                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               b                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                                                               +
|                                                               |
+                               id                              +
|                                                               |
+                                                               +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Given the stack variable id is also with static size, I'm confused why the c++ compiler still looks for a brace-enclosed initializer even if id is already a static-sized array.

Upvotes: 2

Views: 692

Answers (4)

Frodyne
Frodyne

Reputation: 3973

If you want to copy the entire string, you have to use memcopy into conf.id (or strncpy if it is guaranteed to be a zero-terminated string). Unfortunately this means that the id in conf_t cannot be const anymore:

#include <iostream>
#include <cstring>
#include <string>
#define ID_SIZE 16

struct conf_t
{
    const unsigned int a;
    const unsigned int b;
    unsigned char id[ID_SIZE];
};

int main()
{
    const std::string raw_id("0123456789ABCDE");
    conf_t conf = {10, 2048, {0}};
    memcpy(conf.id, raw_id.c_str(), ID_SIZE); // <- memcopy from the string into conf.id

    std::cout << conf.id << '\n';

    std::cout << std::boolalpha;
    std::cout << std::is_pod<conf_t>::value << '\n';
}

On the other hand, if conf_t.id must be const, then I believe you must use a compile-time constant initialization in order to keep conf_t a POD class:

struct conf_t
{
    const unsigned int a;
    const unsigned int b;
    const unsigned char id[ID_SIZE];
};

int main()
{
    conf_t conf = {10, 2048, "0123456789ABCDE"};
...

It is also possible to use a template constructor to turn a dynamic array into an initializer-list. This will enable you to initialize a const c-array with dynamic data, but it adds a constructor to conf_t which means that it no longer is a POD class.

#include <iostream>
#include <cstring>
#include <string>
#include <utility>
#define ID_SIZE 16

struct conf_t
{
    const unsigned int a;
    const unsigned int b;
    const unsigned char id[ID_SIZE];

    conf_t(const unsigned int a,
           const unsigned int b,
           const unsigned char (&arr)[ID_SIZE])
        : conf_t(a, b, arr, std::make_index_sequence<ID_SIZE>())
    {
    }

private:
    template <std::size_t... Is>
    conf_t(const unsigned int a,
           const unsigned int b,
           const unsigned char (&arr)[ID_SIZE], std::index_sequence<Is...>)
        : a{a},
          b{b},
          id{arr[Is]...}
    {
    }
};

int main()
{
    const std::string raw_id("0123456789ABCDE");
    unsigned char id[ID_SIZE];
    memcpy(id, raw_id.c_str(), ID_SIZE);
    conf_t conf = {10, 2048, id};

    std::cout << conf.a << '\n';
    std::cout << conf.b << '\n';
    std::cout << conf.id << '\n';

    std::cout << std::boolalpha;
    std::cout << std::is_pod<conf_t>::value << '\n';
}

It is possible that I have missed something though, so I welcome any corrections.

Upvotes: 1

Micha&#235;l Roy
Micha&#235;l Roy

Reputation: 6471

Something is odd in your declaration. I suspect most of you issues come from an abusive usage of const.

Your conf_t definition reads:

struct conf_t { 
    const unsigned int a;
    const unsigned int b;
    const unsigned char id[ID_SIZE];
};

Why are you declaring constant members? And why are they uninitialized? The fact that they are all declared const and none of them are initialized makes me suspect a big code smell. If you require a constant conf_t, then declare it the usual way, as in:

struct conf_t { 
    unsigned int a;            // note that the const keyword has disappeared.
    unsigned int b;
    unsigned char id[ID_SIZE];
};

// the conf_t variable is declared const..

const conf_t my_conf { 10, 2048, "0123456789ABCDE" };

// et voilà !

If you need more flexibility, (ie: initialize id from an lvalue), you can either define a constructor as suggested by @Frodyne, or create an initializer function, this solution has the advantage of keeping the POD property of you simple conf_t struct.

Example:

struct conf_t { 
    unsigned int a;
    unsigned int b;
    unsigned char id[ID_SIZE];  // is there a VERY good reason why this is unsigned char?
};

// initializes a conf_t, I guess you cpould call that a 
// pseudo-constructor of sorts.

conf_t make_conf(int a, int b, const char* id)
{
     // our contract:
     assert(id != NULL && strlen(id) < ID_SIZE);  // or strlen(id) == ID_SIZE - 1, if you wish

     conf_t result = { a, b };
     strncpy((char*)result.id, id, sizeof(result.id));
     result.id[sizeof(result.id) - 1] = 0;

     // all c++ compilers less than 20 years old will return in-place
     return result;
}


//  declaration from literals then becomes:

const conf_t cfg1 = make_conf(1, 2, "0123456789ABCDE");

//  declaration from l-values becomes:

int x = 123; 
int y = 42;
std::string s = "EDCBA9876543210";

const conf_t cfg2 = make_conf(x, y, s.c_str());

Which shouldn't be too taxing on the fingers, nor on the eyes.

Upvotes: 0

Alex Vergara
Alex Vergara

Reputation: 2209

The error on the initialization is that you cannot initialize an array element of type const unsigned char with an lvalue of type unsigned char[16].

What you need to initialize your member, is to use the correct initialization syntax with curly braces { vals... }, and use the correct types on the initialization values. But, this isn't so easy in standard C++, because you are triying to initialize a const value from a non-const one.

One workaround without losing const in your id member is to simply change it to const unsigned char*. Here the pointer type is the key.

Then, you can use some obscure monsters that exists in C++, like reinterpret_cast<T>, and initialize your const member from a non const one.

#include <cstring>
#include <string>
#define ID_SIZE 16

struct conf_t {
    const unsigned int a;
    const unsigned int b;
    const unsigned char* id[ID_SIZE];
};

int main() {
    const std::string raw_id("0123456789ABCDEF");
    unsigned char id[ID_SIZE];
    memcpy(id, raw_id.c_str(), ID_SIZE);
    struct conf_t conf = {10, 2048, reinterpret_cast<const unsigned char*>(id)};
    std::cout << *(conf.id);
}

And everything compiles fine now. You can see a live example here

Upvotes: 0

Micha&#235;l Roy
Micha&#235;l Roy

Reputation: 6471

Try either of these syntaxes:

struct conf_t {
    const unsigned int a;
    const unsigned int b;
    const unsigned char id[ID_SIZE];
};

conf_t syntax_1 = { 10, 1, { 'a', 'b', 'c' }};  // an array needs its own {}
conf_t syntax_2 = { 10, 1, "hello" };           // an array of char can be a string.
                                                // make sure you have room for the
                                                // null termination!

Upvotes: 1

Related Questions