3

In C++ (or even C for that matter), you can declare:

const char *foo[] = {
        "hello",
        NULL,
        "cruel",
        NULL,
        NULL,
        "world",
};

Is there any way to make this simple and clean initialization syntax work with arrays of (const) integers instead of arrays of chars, aka string constants, while still allowing for NULL?

So instead of strings like "hello" and "world", could you possibly have some kind of initializer list syntax like &{ 1, 4, 12} or &({ 0, 4, 9 }) or whatever? Or are strings just a special case in the syntax?

I looked at Initializing "a pointer to an array of integers", but it didn't help. Also, it was asked 11 years ago, and maybe a more recent C++ standard might provide a way?

Note: This question was originally for both C and C++, but some commenters objected. The interesting answers below are for C++, although a simple C solution was provided in the comments, therefore I have switched the title and tag to C++.


In case it's relevant:

  • The integer data is pre-calculated and unchanging.
  • For lookup purposes, each pointer to an integer array naturally belongs at a specific index position of the array foo of pointers. It is not arbitrary.
  • For each item in some dataset that the user wants to process, they will use a simple math formula to derive an integer numerical value. So if they derive (for example) 0 from the math, then they need to look in foo[0] to find a pointer to the corresponding array (for example) 1, 4, 12.
  • The index positions that contain NULL are mathematically impossible and will never be accessed.
  • Each array of integers will be of the same dimension.
  • However nearly 80% of the pointers will be NULL, so a 2D array would be wasteful.
  • The dimensions will be large-ish, but not so large that reading from a file is the only sensible option. Say, an array of 5,000 pointers to arrays of 1,000 integers (with 80% of the pointers being NULL instead). The include file that declares foo would be generated automatically.
19
  • 2
    sounds like maybe you need a sparse matrix if 80% of the dataset will be empty Commented Jul 30, 2024 at 1:24
  • do you mean like this int foo[] = {1,2,3,4,5}; ? Sparce matrix does seem like something you need here. Commented Jul 30, 2024 at 1:25
  • 2
    Please don't ask for both C and C++ in the same question. The answers are very likely going to be significantly different for the two languages. Commented Jul 30, 2024 at 1:41
  • 1
    @ghostarbeiter Using a compound literal if the declaration is at file scope (but not block scope!), e.g. &(int[]){0,1,2,3}[0]. Beware that some compilers also allow this syntax in C++ mode, although it is not permitted in standard C++, but even if they do, they likely do not give it the correct semantics. Commented Jul 30, 2024 at 1:54
  • 1
    @EricPostpischil I'd prefer an answer in C, because that's what the existing code base is written in. Commented Jul 30, 2024 at 1:54

5 Answers 5

6

Yes, you can do it with variadic non-type template parameters (godbolt):

// 0-terminated, remove ", 0" at the end if you don't want that
template<int... V>
static constexpr int ints[] = { V..., 0 };

const int* foo[] = {
    ints<1, 2, 3>,
    nullptr,
    nullptr,
    ints<4, 5>,
};

int main() {
    for(int i = 0; i < 4; ++i) {
        auto* p = foo[i];
        if (!p) {
            std::cout << "nullptr" << std::endl;
            continue;
        }
        while (*p) std::cout << *p++ << " ";
        std::cout << std::endl;
    }
}

This requires C++17, but can be adapted to work with C++11


EDIT: if array size is fixed, you may use std::array, which gives nice STL container interface (e.g., for-range loop) (godbolt)

using arr_t = std::array<int, 3>;

template<int... V>
static constexpr arr_t ints = { V... };

const arr_t* foo[] = {
    &ints<1, 2, 3>,
    nullptr,
    nullptr,
    &ints<4, 5, 6>,
};
Sign up to request clarification or add additional context in comments.

4 Comments

Compared with using std::unordered_map this will use more memory if a large portion of the slots is empty, but has the likely more important benefit that it will be constant-initialized, i.e. foo will be guaranteed to be initialized at compile-time (which can be further assured by adding constexpr or constinit on its declaration).
@KSH In my application it is actually guaranteed that all the non-NULL-pointered integer arrays are of the same fixed dimension that is known in advance. So in your example you'd have ints<4, 5, 6> (exactly three elements). If there is a simplification for that situation, perhaps it might be interesting to edit your post to add that below your existing more general solution?
@ghostarbeiter sure, added example for fixed-size arrays in the answer.
@user17732522 Yes it depends on data density / access pattern. Another benefits of my approach would be that if two sub-arrays have the same content, they will be stored only once in memory.
4

So if they derive (for example) 0 from the math, then they need to look in foo[0] to find a pointer to the corresponding array (for example) 1, 4, 12.

I think you want std::unordered_map to map between a computed index value and the corresponding array.

Example:

std::unordered_map<size_t, std::vector<int>> foo;
foo[0] = { 1, 4, 12 };
foo[1] = { 0, 4, 9 };
foo[99] = {11,22,33};

Or on newer versions of C++:

std::unordered_map<size_t, std::vector<int>> foo = {
    {0, { 1, 4, 12 }},
    {1, {0,  4,  9 }},
    {99, {11, 22, 33}}
};

And then to reference a particular element:

auto& lookup = foo[99]; // reference assignment
std::cout << lookup[0] << " " << lookup[1] << " " << lookup[2] << std::endl;

The above will print: 11 22 33

If foo[99] doesn't exist, it will return an empty vector after inserting it into the map. So to validate if the key even exists at all:

auto itor = foo.find(99);
if (itor != foo.end()) {
    auto& lookup = itor->second;
    std::cout << lookup[0] << " " << lookup[1] << " " << lookup[2] << std::endl;
}

2 Comments

OP says that the dimension of the element arrays is always the same. So instead of std::vector they can also use std::array. Either way, unfortunately foo cannot currently be constant-initialized, so it will be constructed at runtime (which may or may not be an issue).
@user17732522 - good point. The above code is pretty much interchangeable with foo being declared as std::unordered_map<size_t, std::array<int,3>>
4

In C (since C99), you can use compound literals for this purpose:

const int *const p[] = {(int[]){2, 4}, NULL}; 

It’s harder in C++, but it’s possible to use references lifetime-extended temporaries for a similar effect:

using A=int[2];
struct B {const A &r;};
A null;
extern const B foo[]={A{2,4},null};

None of the references are null (of course), but only one row’s worth of memory is wasted giving them all something to which to refer.

Comments

0

Why don't replace NULL with an empty vector?

    const std::vector<std::vector<int>> a = {
        {1, 2, 3},
        {},
        {4, 5, 6},
        {},
    };

If the index can be large, I'd recommend selbie's answer. std::unordered_map is more suitable in this case.

Comments

0

You can use array of valarray as shown below.

#include <iostream>
#include <valarray>

using namespace std;

int main()
{
  valarray<int> varr2[] {{1,4,8,21},{},{2,5,8}, {3,NULL,5}};

  for(auto& x: varr2) {
    cout << x.size() << endl;
    for(auto& y: x) {
      cout << y << " ";
    }
    cout << endl;
  }

  return 0;
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.