There is an answer to a Stack Overflow question explaining why the above code doesn't compile:
It seems the error is produced according to [expr.const] §2:
An expression e is a core constant expression unless the evaluation of
e, following the rules of the abstract machine (4.6), would evaluate
one of the following expressions:
...
(2.3) — an invocation of an undefined constexpr function or an
undefined constexpr constructor;
How come it is undefined, when the call is clearly after the
definition?
The thing is, member function definitions are delayed until the
closing brace of the outermost enclosing class (because they can see
members of enclosing classes).
A solution to this is to declare square_ as const, and define it as constexpr and initialize it outside the struct. This way, at the point of the square_ definition, the constexpr method build_square() will be already defined. [Demo]
struct Vigenere
{
static constexpr unsigned char letters_size_{ 26 };
using square_t = std::array<std::array<unsigned char, letters_size_>, letters_size_>;
static const square_t square_;
};
/* static */ constexpr Vigenere::square_t Vigenere::square_{ Vigenere::build_square() };
There is this other answer to another Stack Overflow question explaining why you can declare members as const and define them as constexpr:
constexpr pertains only to the definition of a variable. [...]
It implies const (on the variable itself: [...]),
so you haven’t changed the variable’s type.
This is no different from:
// foo.hpp
extern const int x;
// foo.cpp
constexpr int x=2;
Now, is this latter code (static constexpr square_ definition outside the class) just/about as efficient as what would be to have a static constexpr square_ definition within the class? I.e. with something like this: [Demo]
struct Vigenere
{
static constexpr unsigned char letters_size_{ 26 };
using square_t = std::array<std::array<unsigned char, letters_size_>, letters_size_>;
static constexpr square_t square_{{
{ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' },
{ 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a' },
{ 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b' },
{ 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c' },
{ 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd' },
{ 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e' },
{ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f' },
{ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g' },
{ 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' },
{ 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i' },
{ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' },
{ 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k' },
{ 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l' },
{ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm' },
{ 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n' },
{ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o' },
{ 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p' },
{ 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q' },
{ 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r' },
{ 't', 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's' },
{ 'u', 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't' },
{ 'v', 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u' },
{ 'w', 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v' },
{ 'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w' },
{ 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x' },
{ 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y' }
}};
};
We can examine the assembler output in Compiler Explorer:
- Solution creating the table with
static consteval square_t build_square() and defining the table member outside the struct as /* static */ constexpr Vigenere::square_t Vigenere::square_{ Vigenere::build_square() }:
- The table is indeed generated at compile time.
Vigenere::square_:
.byte 97
.byte 98
.byte 99
.byte 100
...
- There is some static initialization and destruction code at the end of the binary, but this code doesn't call
build_square(). I understand it is performing the std::array initialization and destruction, and that this happens during the early and final stages of the program execution, respectively.
__static_initialization_and_destruction_0(int, int):
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
cmp DWORD PTR [rbp-4], 1
jne .L18
cmp DWORD PTR [rbp-8], 65535
jne .L18
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
call __cxa_atexit
.L18:
nop
leave
ret
_GLOBAL__sub_I_main:
push rbp
mov rbp, rsp
mov esi, 65535
mov edi, 1
call __static_initialization_and_destruction_0(int, int)
pop rbp
ret
- Interestingly, if
square_ is defined with const instead of constexpr, the only difference is the GLOBAL__sub_I_main label at the end of the binary, that is now called _GLOBAL__sub_I_Vigenere::square_.
- Solution defining the table as
static constexpr square_t square_{ /* values */ } within the struct.
- The table is also generated at compile time.
Vigenere::square_:
.ascii "abcdefghijklmnopqrstuvwxyz"
.ascii "bcdefghijklmnopqrstuvwxyza"
.ascii "cdefghijklmnopqrstuvwxyzab"
.ascii "defghijklmnopqrstuvwxyzabc"
.ascii "efghijklmnopqrstuvwxyzabcd"
.ascii "fghijklmnopqrstuvwxyzabcde"
.ascii "ghijklmnopqrstuvwxyzabcdef"
.ascii "hijklmnopqrstuvwxyzabcdefg"
.ascii "ijklmnopqrstuvwxyzabcdefgh"
.ascii "jklmnopqrstuvwxyzabcdefghi"
.ascii "klmnopqrstuvwxyzabcdefghij"
.ascii "lmnopqrstuvwxyzabcdefghijk"
.ascii "mnopqrstuvwxyzabcdefghijkl"
.ascii "nopqrstuvwxyzabcdefghijklm"
.ascii "opqrstuvwxyzabcdefghijklmn"
.ascii "pqrstuvwxyzabcdefghijklmno"
.ascii "qrstuvwxyzabcdefghijklmnop"
.ascii "rstuvwxyzabcdefghijklmnopq"
.ascii "stuvwxyzabcdefghijklmnopqr"
.ascii "tuvwxyzabcdefghijklmnopqrs"
.ascii "uvwxyzabcdefghijklmnopqrst"
.ascii "vwxyzabcdefghijklmnopqrstu"
.ascii "wxyzabcdefghijklmnopqrstuv"
.ascii "xyzabcdefghijklmnopqrstuvw"
.ascii "yzabcdefghijklmnopqrstuvwx"
.ascii "zabcdefghijklmnopqrstuvwxy"
- The static initialization and destruction code at the end of the binary seems exactly the same as in the first case.
__static_initialization_and_destruction_0(int, int):
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
cmp DWORD PTR [rbp-4], 1
jne .L18
cmp DWORD PTR [rbp-8], 65535
jne .L18
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
call __cxa_atexit
.L18:
nop
leave
ret
_GLOBAL__sub_I_main:
push rbp
mov rbp, rsp
mov esi, 65535
mov edi, 1
call __static_initialization_and_destruction_0(int, int)
pop rbp
ret
Conclusion: using a consteval method to create the table, declaring square_ as const within the struct, and defining it as constexpr and initializing it outside the struct:
- Creates the table at compile time.
- Produces exactly the same code as declaring
square_ as constexpr, and defining it and initializing it within the struct (the assembly for the table definition is different, one uses .byte, the other .ascii, but the binary code should be the same).