Search code examples
c++lambdac++20

clang constexpr compile error in lambda, gcc and msvc ok. clang bug?


I ran across a compile error in clang with this code. I don't see any problem with it and it works in msvc and gcc. Am I missing something or is it a clang bug?

#include <iostream>
#include <string>
#include <type_traits>

constexpr bool do_tests()
{
    // prints error message at runtime if test is false
    auto verify = [](bool test, std::string message = "error") {
        if (!std::is_constant_evaluated() && !test)
            std::cout << message << '\n';
        return test;
    };
    return verify(1 == 1, "test");
};


int main()
{
    constexpr bool b = do_tests();
    std::cout << b << '\n';
}

compiler explorer

Inexplicable error message from clang:

basic_string.h:356:10: note: assignment to member '_M_local_buf' of union with no active member is not allowed in a constant expression


Solution

  • There is no problem with the code and Clang also compiles it correctly if you use libc++ (-stdlib=libc++). Compiler explorer by default uses libstdc++ for Clang (-stdlib=libstdc++). There simply seems to be a compatibility issue between the two. My guess is that libstdc++ is implementing the constexpr string in a way that is technically not allowed in constant expressions, but GCC tends to be less strict in that regard than Clang is, so that Clang fails with the implementation while GCC doesn't have a problem with it.


    To be more precise in libstdc++ the implementation of std::string contains an anonymous union with one member being a _CharT array named _M_local_buf. Then libstdc++ is doing the following if the evaluation happens in a constant expression evaluation (from https://github.com/gcc-mirror/gcc/blob/releases/gcc-12.2.0/libstdc++-v3/include/bits/basic_string.h#L356)

    for (_CharT& __c : _M_local_buf)
        __c = _CharT();
    

    in order to set the union member _M_local_buf to active and zero it.

    The problem is that prior to this loop no member of the union has been set to active and setting a member (subobject) through a reference, rather than directly by name or through member access expressions, is technically not specified in the standard to set a member to active. As a consequence that technically has undefined behavior, which is why Clang (correctly) doesn't want to accept it as constant expression. If the member was set as active beforehand, e.g. with an extra line _M_local_buf[0] = _CharT(); it would be fine.

    Now of course it seems a bit silly that setting _M_local_buf[0] directly is fine, but setting it through a reference __c isn't, but there may be good reasons for this. A reference cannot generally be guaranteed to refer to a specific object known at compile-time and so it might make it harder for a compiler to recognize whether or not the expression needs to restrict certain optimizations around it which wouldn't be valid if the active member is changed.


    There is a libstdc++ bug report relating to a very similar issue here, but that is already supposed to be fixed for a while.

    It seems that the commit https://github.com/gcc-mirror/gcc/commit/98a0d72a610a87e8e383d366e50253ddcc9a51dd has (re-)introduced the particular issue you are seeing here. Before the commit the member was set active correctly. Might be worth it to open an issue about this, although as mentioned above Clang is being very pedantic here and as the linked bug report also says, the standard should probably be changed to allow this.


    UPDATE:

    There is a commit for libstdc++ in GCC releases 13.1 and 12.3 that does fix the issue when compiling with Clang. See commit https://github.com/gcc-mirror/gcc/commit/52672be7d328df50f9a05ce3ab44ebcae50fee1b:

    _M_local_buf[__i] = _CharT();
    

    With an implicit (*this). added to _M_local_buf this now satisfies the form required in class.union.general/5 to start the lifetime of the _M_local_buf array member and therefore set it as active.