Unable to update std::variant in consteval context

139 views Asked by At

I was writing a compile-time parser but I am stuck on a problem which I don't know how to solve in C++. I am using Microsoft Visual Studio Community 2019, version 17.8.3 (latest).

This is the code which is giving me a compiler error:

#include <array>
#include <variant>

using namespace std;

enum class TokenErrors
{
    UNINITIALISED,
    TK_ERROR_SYMBOL,
    TK_ERROR_PATTERN,
    TK_EOF
};

enum class MyTokenType
{
    TK_SYMBOL
};

template <typename TokenType, int num_states>
struct Lexer
{
    std::array<TokenType, num_states> final_states;
};

static consteval auto foo()
{
    auto lxr = Lexer<std::variant<MyTokenType, TokenErrors>, 5>{};
    for (auto &x: lxr.final_states)
        x = TokenErrors::UNINITIALISED;
    return lxr;
}

int main()
{
    auto res = foo();
}

I tried running this code. Visual Studio is giving me this error while compiling:

Build started at 17:22...
1>------ Build started: Project: EfficientCompiler, Configuration: Debug x64 ------
1>Scanning sources for module dependencies...
1>main.cpp
1>C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19): error C2440: '<function-style-cast>': cannot convert from 'initializer list' to 'Lexer<std::variant<MyTokenType,TokenErrors>,5>'
1>C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>Invalid aggregate initialization
1>  C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>  'initializing': cannot convert from 'initializer list' to 'MyTokenType'
1>      C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>      Too many braces around initializer for 'MyTokenType'
1>  C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>  'initializing': cannot convert from 'initializer list' to 'MyTokenType'
1>      C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>      Too many braces around initializer for 'MyTokenType'
1>  C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>  'initializing': cannot convert from 'initializer list' to 'MyTokenType'
1>      C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>      Too many braces around initializer for 'MyTokenType'
1>  C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>  'initializing': cannot convert from 'initializer list' to 'MyTokenType'
1>      C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>      Too many braces around initializer for 'MyTokenType'
1>  C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>  'initializing': cannot convert from 'initializer list' to 'MyTokenType'
1>      C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,19):
1>      Too many braces around initializer for 'MyTokenType'
1>C:\Users\Aakash\source\repos\EfficientCompiler\EfficientCompiler\main.cpp(35,16): error C7595: 'foo': call to immediate function is not a constant expression
1>Done building project "EfficientCompiler.vcxproj" -- FAILED.
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========
========== Build completed at 17:22 and took 01.116 seconds ==========

When I convert the signature of function foo() to static constexpr auto foo(), the code compiles fine.

I am stuck on this problem, because as far as I know, std::variant is constexpr compatible (and it is compiling fine in constexpr context too) and it should not result in this error in consteval. Can someone help me here?

2

There are 2 answers

2
Adrian Mole On

As mentioned in the comments, your code looks fine and this appears to be a bug in the MSVC compiler (which still persists in the version that ships with the 'latest' Visual Studio 2022). However, this question does not have the tag and I'm not going to try to give an authoritative explanation for why I consider it a bug (other than the fact that your code compiles on every other compiler I have tried).

The strictly correct way to fix this is to file a bug report with the Visual Studio team and wait for them to fix the problem. But that could be a (very) long wait.

In the meantime, I will suggest a possible workaround …

Some investigations revealed that, in your code, what appears to be upsetting/confusing the MSVC compiler is the attempt to initialize the elements of the final_states array with values that are not the first variant type. Thus, if you change the x = TokenErrors::UNINITIALISED; line in the for loop to x = MyTokenType::TK_SYMBOL;, your code then compiles. However, this makes a significant – and very likely undesirable – change to the code base.

An alternative is to redefine the order of the std::variant types, so that TokenErrors becomes the first (i.e. default) object type. (This may also be undesirable, but it has less observable effects that the previous suggestion.) The following version of your foo function compiles with the MSVC compiler in the VS-2019 (142) Toolset (with all other code being left unchanged from that you have posted):

static consteval auto foo()
{
    auto lxr = Lexer<std::variant<TokenErrors, MyTokenType>, 5>{};// Swap type order
    for (auto& x : lxr.final_states) {
        x = TokenErrors::UNINITIALISED; // Now first variant type!
    }
    return lxr;
}

This may also not be an 'acceptable' change. If so, please let me know and I will attempt to find alternative workarounds. I have already tried many other tricks, including creating a local, const object of the appropriate std::variant type and using that to initialize the x in the loop, but all these attempts have (so far) been unsuccessful.


Here is one of those attempts, using the explicit constructor of std::variant that takes a std::in_place_index<n> as its first argument; with this, the code as presented compiles but, if the first line is replaced with the (commented-out) second, it fails with the error message you have cited:

static consteval auto foo()
{
    const std::variant<MyTokenType, TokenErrors> var{ std::in_place_index<0>, MyTokenType::TK_SYMBOL };     // Works
//  const std::variant<MyTokenType, TokenErrors> var{ std::in_place_index<1>, TokenErrors::UNINITIALISED }; // Fails
    auto lxr = Lexer<std::variant<MyTokenType, TokenErrors>, 5>{ };
    for (auto& x : lxr.final_states) {
        x = var;
    }
    return lxr;
}

Note: In the above code (the last snippet), and elsewhere, replacing the loop with lxr.final_states.fill(var); also makes no difference.

0
Aakash Gupta On

I reported the bug here: https://github.com/microsoft/STL/issues/4286

It seems that the problem exists for almost 2.5 years now and there is no resolution yet.

As a workaround, as suggested in the GitHub link, the following code will compile without any problems:

constexpr auto res = foo();