constexpr array of constexpr objects using move ct

2019-06-16 07:34发布

问题:

I have a class with a constexpr value constructor, but no copy or move ctor

class  C {
    public:
        constexpr C(int) { }
        C(const C&) = delete;
        C& operator=(const C&) = delete;
};

int main() {
    constexpr C arr[] = {1, 2};
}

I've found that this code doesn't work because it's actually trying to use the move constructor for C rather than the value constructor to construct in place. One issue is that I want this object to be unmovable (for test purposes) but I thought "okay, fine, I'll add a move constructor."

class  C {
    public:
        constexpr C(int) { }
        C(const C&) = delete;
        C& operator=(const C&) = delete;
        C& operator=(C&&) = delete;

        C(C&&) { /*something*/ } // added, assume this must be non trivial
};

Okay fine, now it uses the move constructor and everything works under gcc but when I use clang, it complains because the move constructor is not marked constexpr

error: constexpr variable 'arr' must be initialized by a constant expression
    constexpr C arr[] = {1, 2};

If I mark the move constructor constexpr it works under gcc and clang, but the issue is that I want to have code in the move constructor if it runs at all, and constexpr constructors must have empty bodies. (The reason for my having code in the move ctor isn't worth getting into).

So who is right here? My inclination is that clang would be correct for rejecting the code.

NOTE

It does compile with initializer lists and non-copyable non-movable objects as below:

class  C {
    public:
        constexpr C(int) { }
        C(const C&) = delete;
        C& operator=(const C&) = delete;
        C& operator=(C&&) = delete;
        C(C&&) = delete;

};

int main() {
    constexpr C arr[] = {{1}, {2}};
}

My main concern is which compiler above is correct.

回答1:

So who is right here?

Clang is correct in rejecting the code. [expr.const]/2:

A conditional-expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine (1.9), would evaluate one of the following expressions:

  • an invocation of a function other than a constexpr constructor for a literal class, a constexpr function, or an implicit invocation of a trivial destructor (12.4)

Clearly your move constructor isn't a constexpr constructor - [dcl.constexpr]/2

Similarly, a constexpr specifier used in a constructor declaration declares that constructor to be a constexpr constructor.

And the requirements for an initializer of a constexpr object are in [dcl.constexpr]/9:

[…] every full-expression that appears in its initializer shall be a constant expression. [ Note: Each implicit conversion used in converting the initializer expressions and each constructor call used for the initialization is part of such a full-expression. — end note ]

Finally the move constructor is invoked by the copy-initialization of the array elements with the corresponding initializer-clauses - [dcl.init]:

Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call initializes a temporary of the cv-unqualified version of the destination type. The temporary is a prvalue. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization.

In the second example, copy-list-initialization applies - and no temporary is introduced.

By the way: GCC 4.9 does not compile the above, even without any warning flags provided.



回答2:

§8.5 [dcl.init]/p17:

The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.

  • If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).
  • [...]
  • If the destination type is a (possibly cv-qualified) class type:
    • If the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, [...]
    • Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call initializes a temporary of the cv-unqualified version of the destination type. The temporary is a prvalue. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization. In certain cases, an implementation is permitted to eliminate the copying inherent in this direct-initialization by constructing the intermediate result directly into the object being initialized; see 12.2, 12.8.
  • [...]

§8.5.1 [dcl.init.aggr]/p2:

When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list are taken as initializers for the members of the aggregate, in increasing subscript or member order. Each member is copy-initialized from the corresponding initializer-clause. If the initializer-clause is an expression and a narrowing conversion (8.5.4) is required to convert the expression, the program is ill-formed. [ Note: If an initializer-clause is itself an initializer list, the member is list-initialized, which will result in a recursive application of the rules in this section if the member is an aggregate. —end note ]

§8.5.4 [dcl.init.list]/p3:

List-initialization of an object or reference of type T is defined as follows:

  • If T is an aggregate, aggregate initialization is performed (8.5.1).
  • [...]
  • Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.
  • [...]

For constexpr C arr[] = {1, 2};, aggregate initialization copy-initializes each element from the corresponding initializer-clause, i.e., 1 and 2. As described in §8.5 [dcl.init]/p17, this constructs a temporary C and then direct-initializes the array element from the temporary, which requires an accessible copy or move constructor. (The copy/move can be elided, but the constructor must still be available.)

For constexpr C arr[] = {{1}, {2}};, the elements are copy-list-initialized instead, which does not construct temporaries (note the absence of any mention of a temporary being constructed in §8.5.4 [dcl.init.list]/p3).