Search code examples
c++c++20c++-concepts

Static constexpr data member with templated IIFE does not check requires-clause


My problem:

Hi everyone,

I wrote some code that compiles, and I don't know why. Here is a minimal example:

#include <vector>

template <typename T>
concept IsIncrementable = requires(T a) { // Dummy concept for the example
    ++a;
};

template<typename T>
struct Test
{
    static constexpr int a = [](auto x) -> int requires IsIncrementable<T> { return 0; }(0);
};


int main()
{
    // T = std::vector<int>
    // It does not satisfy IsIncrementable<T>
    // Why does this code compiles ? 
    Test<std::vector<int>> t;
    return 0;
}

Essentially, a variable is initialized through IIFE. Here are some indication about this code:

  1. Yes, the requires clause is on T, not on decltype(x).
  2. No, I DON'T want the requires clause to be on the Test class. Please, do not tell me I can do template <IsIncrementable T> struct Test {}; instead or with requires. I know. And in the full example, I can not do such things...
  3. It is important that the operator() of the lambda carries the requires clause, as in the more general context I can not directly template the lambda itself (other than with auto x).
  4. The variable x is here to allow for the requires clause to exist; otherwise the compiler may indicate that the requires clause shall only appear on templated code.

I thought the code might prevent Test to be instantiated with non-incrementable types. It turns out that I can do:

Test<std::vector<int>> t;

And I can use the a class, until I directly use the member variable, ie I use t.a or Test<std::vector<int>>::a.

Questions:

  1. Is this allowed by the standard not to check this requires clause?
  2. Or to only check this when the variable is used?
  3. If so, can you provide the section(s)?
  4. What is so special with the auto keyword here (see supplementary experiments)? I assume this have to do with the compiler deducing the type.
  5. Is there anything special about initialization with IIFE in this context?
  6. In fact, I am wondering as I am writing this post: Is it even legal to use IIFE in this context?

Thank you in advance.

Supplementary experiments:

  • Using consteval / thread_local / volatile does not change the behavior.
  • Introducing some runtime-only code in the lambda (specifically: return *(new int);) does not change the behavior either.
  • Changing the requires clause to: requires IsIncrementable && false; or with any concept always evaluating to false, even if it involves decltype(x) does not change the behavior.
  • Using less-trivial type for a, eg. std::vector, std::string, does not change the behavior.
  • G++ and Clang both have the same behavior.
  • MSVC never allows for Test<std::vector<int>>
  • Removing the variable x: the code compiles. It means that surprisingly, I can write a non-templated function with a requires clause. Note that this appears to be a bug in gcc/clang, unless you answer false to question 7 (see below).

What DOES change the behavior however, is if I use auto instead: static constexpr auto a = ... makes it impossible to instantiate Test<std::vector<int>>, which is, at least to me, the expected behavior.


Solution

  • Static data members are instantiated independently of the rest of the class. Your program isn't using Test<T>::a anywhere, so it's simply not instantiated. If you did this:

    int main()
    {
        return Test<std::vector<int>>::a;
    }
    

    then you'd get the error that you expected.

    Additionally, when you did this:

    What DOES change the behavior however, is if I use auto instead: static constexpr auto a = ...

    Now analyzing a requires evaluating the expression to determine its type, which leads to earlier instantiation. That just wasn't necessary when you explicitly specified int, so it didn't happen yet.