2

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.

9
  • FYI, this is all C++ code! Don't tag this as C, also see the description of the two tags. That said, you don't write class std::string str, so why do you write struct conf_t conf? Commented Nov 15, 2022 at 11:16
  • @UlrichEckhardt Thanks for the information. The C tag has been removed. I have to define struct conf_t conf, as the current C++ solution for OpenCL kernel code is unstable. Also, I could find most of the documentation and tutorials about OpenCL writing kernel code in C. Therefore, I must craft a c-style struct to pass configuration information from the C++ host to the C kernel code. Commented Nov 15, 2022 at 11:19
  • Sorry, should have been more explicit: You can write conf_t conf, no need for the struct. Commented Nov 15, 2022 at 11:20
  • @UlrichEckhardt Thanks for the suggestion. In real use-case, the struct keyword comes from the C-header included by the kernel code. Under this ground, if I copy and paste the exact struct definition with the keyword struct removed, am I still expecting the memory layout between the c++ host and the c kernel be identical? Commented Nov 15, 2022 at 11:23
  • A POD struct in C++ will have the same layout as a struct in C. (So they'll be API compatible for the ABI.) See std::is_pod and note that the check has been decomposed into std::is_standard_layout and std::is_trivial. Commented Nov 15, 2022 at 11:33

4 Answers 4

1

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.

Sign up to request clarification or add additional context in comments.

2 Comments

I also think there is no way - either no const or initialization by literal, there is no way how you can assign a C-style array to another.
conf_t conf = {10, 2048}; memcpy(conf.id, id, sizeof(conf.id)); works. It's two lines of code, though.
1

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!

2 Comments

Thanks, Michael. syntax_2 looks like what I'm looking for, except for the null termination. I wonder if it can initialize the array id without the null termination? GIven the array size is fixed and guaranteed to have full length, there's no need for a null termination.
You already account for null termination, since "0123456789ABCDE" has 15 characters + 1 numll terminating character. A bit of advice: ALWAYS use null terminated strings. Every single function of the standard C library AND of the C++ library AND OpenCL will expect null-terminated string. The libraries do not know the length of your strings, and expect that null terminator. It's up to you to decide what you want to do: write a 100 lines of software or rewrite thousands of lines of code that is already in the library, but needs that one extra byte.
0

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

4 Comments

This is incorrect! {*id} initializes only the first element of conf_t::id by the first value of id i.e. character '0', the rest of the array is set to 0.
@Quimby completly right. Edited
Why did you change the definition of conf_t::id . OP wants an array of 16 characters, not a array of 16 pointers to character arrays. This is all wrong, the declaration syntax is also wrong. You can only assign an array with a literal value wgen using this syntax, else only a memcpy() will work, as in conf_t conf = {10, 2048}; memcpy(conf.id, id, sizeof(conf.id));.
@TJM do not use this solution as is. the declaration of conf is not what you intended, and will be invalid as soon as char array id goes out of scope. Which means you cannot copy conf to use it outside of the original scope. That's the very bug you want to avoid just waiting to happen.
0

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.

3 Comments

Hi Michael, I declare it with const because I'd like to make the config struct immutable once it's been initialized with actual contents. Given that the real project involves multiple participants, I'd like the compiler to check that there are no writes to immutable configurations, especially when some of my co-workers are unfamiliar with C.
Declaring const variables is for runtime constants.. Declaring const data members is for declaring compile-time constants, they usually are "static" data members, to boot. Trying to do the opposite of what the language features are made for will only make your coding experience, and by extension, your life, a real pain.
For example: how much time would you have saved already if the data members were NOT declared constant, taking into account that a simple memcpy(conf.id, id, SIZE_ID); would have worked.first try? Using const data members, the only option for settnig the value of a conf_t is by literal assignment, which can never fit your requirements.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.