rozina
rozina

Reputation: 4232

Static constexpr array of class objects inside the class itself

Is it possible to have something like this in C++:

struct Foo
{
    int x;
    constexpr Foo(int x) : x(x) {}

    static constexpr Foo table[] =
    {
        Foo(0),
        Foo(1),
        Foo(2),
    };
};

I have tried several combination, but none work. It works if table is not part of Foo class, however I would really like it to be part of the Foo namespace.


Edit:

The reason I want this is so I can access the table as Foo::table. I have several classes like this in a namespace and it is really convenient if I can import the class I am using by writing using someNamespace::Foo and then access the table as Foo::table. If the table is outside the class I have to always access it by writing someNamespace::fooTable.

Upvotes: 12

Views: 1258

Answers (2)

Vittorio Romeo
Vittorio Romeo

Reputation: 93394

The compiler error is clear here:

error: invalid use of incomplete type 'struct Foo'
         Foo(0),
              ^
note: definition of 'struct Foo' is not complete until the closing brace
 struct Foo
        ^~~

Foo is considered an "incomplete type" until the closing brace of its definition is reached. The size of incomplete types is not known, so the compiler doesn't know how much space table would require.


Here's a workaround:

struct FooTable
{
    constexpr auto operator[](int n) const;
};  

struct Foo
{
    int x;
    constexpr Foo(int x) : x(x) {}

    constexpr static FooTable table{};
};

constexpr auto FooTable::operator[](int n) const
{
    constexpr Foo table[] =
    {
        Foo(0),
        Foo(1),
        Foo(2),
    };

    return table[n];
}

live example on wandbox

Usage:

int main()
{
    constexpr auto x = Foo::table[1];
}

If you don't want Foo to be copied, you can place table inside a "detail" namespace and then return const auto& from FooTable::operator[] - example here.

Upvotes: 8

nh_
nh_

Reputation: 2241

You can use the following trick, which basically moves the table to a templated wrapper, which is instantiated only when the class definition of Foo is complete.

template<typename T>
struct Wrapper
{
    static constexpr T table[] = { T(0), T(1), T(2) };
};

struct Foo : public Wrapper<Foo>
{
    int x;
    constexpr Foo(int x) : x(x) {}
};

Not sure whether this is actually an acceptable workaround in your situation, but it is how you can get your example to compile and run.

If you want to specify the initialization values of your table entries within the Foo class, you can extend the wrapper to take those values:

template<typename T, int... Args>
struct Wrapper
{
    static constexpr T table[] = { T(Args)... };
};

struct Foo : public Wrapper<Foo, 0, 1, 2>
{
    int x;
    constexpr Foo(int x) : x(x) {}
};

In both cases you can have all your classes derive from Wrapper without the need to define additional ones, since the statics of Wrapper exist per instantiation. If you need your entries to take values other than int, you can also pass that type as another template argument:

template<typename T, typename A, A... Args>
struct Wrapper
{
    static constexpr T table[] = { T(Args)... };
};

struct Foo : public Wrapper<Foo, int, 0, 1, 2>
{
    int x;
    constexpr Foo(int x) : x(x) {}
};

struct Bar : public Wrapper<Bar, char, 'a', 'b', 'c'>
{
    char x;
    constexpr Bar(char x) : x(x) {}
};

Passing multiple arguments to each constructor is achievable as well. Use a std::pair or some other wrapper to group them in that case.

Upvotes: 6

Related Questions