15

Is the following code legitimate?

template <int N>
class foo {
public:
    constexpr foo()
    {
        for (int i = 0; i < N; ++i) {
            v_[i] = i;
        }
    }

private:
    int v_[N];
};

constexpr foo<5> bar;

Clang accepts it, but GCC and MSVC reject it.

GCC's error is:

main.cpp:15:18: error: 'constexpr foo<N>::foo() [with int N = 5]' called in a constant expression
   15 | constexpr foo<5> bar;
      |                  ^~~
main.cpp:4:15: note: 'constexpr foo<N>::foo() [with int N = 5]' is not usable as a 'constexpr' function because:
    4 |     constexpr foo()
      |               ^~~
main.cpp:4:15: error: member 'foo<5>::v_' must be initialized by mem-initializer in 'constexpr' constructor
main.cpp:12:9: note: declared here
   12 |     int v_[N];
      |         ^~

If this kind of code were OK, I could cut quite a few uses of index_sequences.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
Yongwei Wu
  • 5,292
  • 37
  • 49
  • 1
    [Gcc10](https://wandbox.org/permlink/yq7TTJA8wXWyKomV) accepts it too. – songyuanyao Jan 09 '20 at 10:59
  • could you transcript the error from MSVC? – max66 Jan 09 '20 at 10:59
  • ... and GCC, too. – Evg Jan 09 '20 at 10:59
  • 1
    @songyuanyao - g++10 accept it compiling C++20; refuses it compiling C++17 or older; the point seems that `_v` should be initialized in initialization list, until C++17. Maybe is changed something in C++20. – max66 Jan 09 '20 at 11:05
  • If `constexpr foo<5> bar;` appears inside a scope, it [fails](https://godbolt.org/z/NaZaDp) with Clang, too. – Evg Jan 09 '20 at 11:17
  • 2
    @Evg That's actually interesting, because it may suggest Clang uses its "awareness" that a static-storage-duration object gets zeroed out to say "okay, this object may have been default-initialised but reads from its `int` member will never have undefined behaviour". I wonder whether GCC _not_ doing that is compliant, or the other way around... – Lightness Races in Orbit Jan 09 '20 at 11:21
  • I noticed that Clang does not accept it in the scope too. Since my main usage is to make some compile-time calculation and use the result in the whole program, that behaviour is OK to me. – Yongwei Wu Jan 09 '20 at 12:57

2 Answers2

17

Trivial default initialisation was prohibited in a constexpr context until C++20.

The reason, I'm guessing, is that it is easy to "accidentally" read from default-initialised primitives, an act which gives your program undefined behaviour, and expressions with undefined behaviour are straight-up prohibited from being constexpr (ref). The language has been extended though so that now a compiler must check whether such a read takes place and, if it doesn't, the default-initialisation should be accepted. It's a bit more work for the compiler, but (as you've seen!) has substantial benefits for the programmer.

This paper proposes permitting default initialization for trivially default constructible types in constexpr contexts while continuing to disallow the invocation of undefined behavior. In short, so long as uninitialized values are not read from, such states should be permitted in constexpr in both heap and stack allocated scenarios.

Since C++20, it's legal to leave v_ "uninitialised" like you have. Then you've gone on to assign all its elements values, which is great.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • 4
    @max66 Me too! All I did was scan the C++20 changes list on Wikipedia, find something relevant to `constexpr`, and skim the linked proposal ;) – Lightness Races in Orbit Jan 09 '20 at 11:14
  • 3
    The bad part is that are more than 20 years I'm using C++. If every day I learn something new... or I am a bad programmer or C++ is become too complicated. – max66 Jan 09 '20 at 11:17
  • 5
    @max66 It's almost certainly the latter. Also the fact that it keeps fundamentally changing every couple of years makes it a fast moving target. Who can keep up with that?! Even the compilers don't keep up with that. – Lightness Races in Orbit Jan 09 '20 at 11:19
  • @max66 This paper comes to mind: [Remember the Vasa!](http://www.stroustrup.com/P0977-remember-the-vasa.pdf) – Evg Jan 09 '20 at 11:39
  • @max66 I think it is because C++ is just too complicated. For people other than those sitting on the committee, no one know everything about C++. Even those people may miss one thing or two… – Yongwei Wu Jan 09 '20 at 12:50
  • @Evg - Good catch. Stroustrup focuses my concern perfectly. – max66 Jan 09 '20 at 12:58
  • @YongweiWu - I suppose you're right. And this comforts me a little. – max66 Jan 09 '20 at 13:00
  • @max66 It is more than 20 years since I first wrote in C++ too :-). – Yongwei Wu Jan 09 '20 at 13:08
1

While this does not directly answer your question, I thought it was at least worth mentioning. You could simply use in-class initialization and zero-initialize the array:

int v_[N]{};

Another way, without initializing the array first, is to (privately) inherit from std::array. Oddly enough, this actually gets accepted by GCC but not by Clang:

#include <array>

template<int N>
struct foo : private std::array<int, N> {
    constexpr foo() {
        for (auto i = int{}; i < N; ++i) {
            (*this)[i] = i;
        }
    }
};

constexpr foo<5> bar;
303
  • 2,417
  • 1
  • 11
  • 25
  • Zero-initialization is the way to go in C++17 (I've learnt it since I posted the question). It surprised me that your second piece of code is accepted by GCC. Anyway, the way to make it accepted by all is initialize the base class in the constructor: `constexpr foo() : std::array() {…}`. – Yongwei Wu Nov 18 '21 at 14:54
  • The second code snippet was an attempt to find a way to initialize the array without (unnecessarily) zero-initializing it first. It seemed to work but I forgot to test it with it Clang :D – 303 Nov 19 '21 at 01:39