c++g++operator-overloadingclang++assignment-operator

Is this assignment to brace-enclosed string constant really illegal in C++?


I am porting a large codebase from Linux/g++ to MacOS/Clang. I hit this compiler error in multiple places in Clang (where g++ builds successfully and does the right thing at run time):

error: initializer-string for char array is too long, array size is 1 but initializer has size X (including the null terminating character)

Note that I'm compiling for c++14.

I've reduced it to a manageable, reproducible case (eliminating all the uninteresting constructors and other methods), where the errors happen in the WTF constructor's assignments:

#include <stddef.h>

struct StringLike
{
    const char  *str;
    size_t      len;
    
    StringLike() : str(NULL), len(0) {}
    template <size_t LEN_> StringLike(const char (&litAry)[LEN_]) noexcept :
        str(litAry), len(LEN_ - 1) {}
    
    StringLike &operator=(const StringLike &rhs)
        {str = rhs.str; len = rhs.len; return *this;}
    template <size_t LEN_> StringLike &operator=(const char (&strLit)[LEN_])
        {str = strLit; len = LEN_ - 1; return *this;}
    const char *data() const {return str;}
    size_t length() const {return len;}
};

struct WTF
{
    StringLike litStrs[3];
    WTF()
    {
        litStrs[0] = {"Is "};
        litStrs[1] = {"this "};
        litStrs[2] = {"legal?"};
    }
};

Yes, I know I could remove the braces from the litStrs[𝒏], and it does work, but I'd like to not change too many lines of code unnecessarily.

I can't figure how Clang is hallucinating a char array of size 1?!? I do see that if I comment out the StringLike templated constructor, I get a similar error from g++; in the working case, g++ code is converting e.g. {"Is "} to a StringLike temporary via that constructor, then passing that to the operator=(const StringLike &rhs) method (note that removing the braces from the WTF constructor's assignments causes the templated operator= method to be invoked directly, instead).

I'm not really sure where to look in the standard to figure out how the WTF brace-enclosed assignments (operator=, not construction) are supposed to be handled, so I'm not sure whether Clang or g++ is right (Clang has a better track record IMO, but I'm stumped what the correct behavior is here).

I used godbolt to verify that all versions of g++ accept this code, and all versions of Clang complain and give up.


Solution

  • Clang is correct, although the behavior is quite surprising.

    [expr.assign] p8 explains:

    A braced-init-list B may appear on the right-hand side of

    • an assignment to a scalar of type T, in which case B shall have at most a single element. The meaning of x = B is x = t, where t is an invented temporary variable declared and initialized as T t = B.
    • an assignment to an object of class type, in which case B is passed as the argument to the assignment operator function selected by overload resolution ([over.assign], [over.match]).

    Since StringLike is a class type, the second bullet applies. [over.match.oper] p2 explains that litStrs[0] = {"Is "} would be translated into (litStrs[0]).operator=({"Is "});, which Clang also rejects with the same error message.

    The problem lies with how function template argument deduction works in this scenario; LEN_ would have to be deduced from the given {"Is "}. [temp.deduct.call] p1 explains:

    In the P′[N] case, if N is a constant template parameter, N is deduced from the length of the initializer list.

    In the case of {"Is "} and all your other attempts, the length of the initializer list is 1, so Clang is not hallucinating. You're really trying to initialize a const char[1] with a string literal of some other length at this point.

    Possibly a defect

    This behavior is quite surprising and possibly defective because usually, you can use extra braces when initializing an array with a string literal.

    [dcl.init.string] explains that an array may be initialized by a string-literal, or by

    an appropriately-typed string-literal enclosed in braces ([lex.string])

    In other words, initialization of the parameter with "Is " and {"Is "} would be valid if it wasn't for template argument deduction breaking this.