(The links in this answer point to N4659, the C++17 final draft. However, at the time of this writing, the situation is exactly the same for C++20.)
Yes, it's possible. There are two cases:
Case 1
T
is a non-union aggregate for which zero-initialization, followed by default-initialization if the aggregate has a non-trivial constructor, differs from copy-initialization from {}
.
We can use std::in_place_t
to construct our example, because it has an explicit default constructor. Minimal reproducible example:
#include <utility>
struct A {
std::in_place_t x;
};
int main()
{
A(); // well-formed
A{}; // ill-formed
}
(live demo)
Case 1, variant
T
is a union aggregate for whose first element default-initialization differs from copy-initialization from {}
.
We can change struct
to union
in Case 1 to form a minimal reproducible example:
#include <utility>
union A {
std::in_place_t x;
};
int main()
{
A(); // well-formed
A{}; // ill-formed
}
(live demo)
Case 2
T
is of the form const U&
or U&&
where U
can be list-initialized from {}
.
Minimal reproducible example:
int main()
{
using R = const int&;
R{}; // well-formed
R(); // ill-formed
}
(live demo)
Detailed explanation
T()
Per [dcl.init]/17:
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 or is =
braced-init-list, the object or reference is list-initialized.
If the destination type is a reference type, see [dcl.init.ref].
If the destination type is an array of characters, an array of char16_t
, an array of char32_t
, or an array of wchar_t
, and
the initializer is a string literal, see [dcl.init.string].
If the initializer is ()
, the object is value-initialized.
[...]
We can conclude that T()
always value-initializes the object.
T{}
Per [dcl.init]/17:
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.
That's enough for us to conclude that T{}
always list-initializes the object.
Now let's go through [dcl.init.list]/3. I have highlighted the possible cases. The other cases are not possible because they require the initializer list to be non-empty.
List-initialization of an object or reference of type T
is defined
as follows:
(3.1) If T
is an aggregate class and the initializer list has a single element of type cv U
, where U
is T
or a class derived from
T
, the object is initialized from that element (by
copy-initialization for copy-list-initialization, or by
direct-initialization for direct-list-initialization).
(3.2) Otherwise, if T
is a character array and the initializer list has a
single element that is an appropriately-typed string literal
([dcl.init.string]), initialization is performed as described in that
section.
(3.3) Otherwise, if T
is an aggregate, aggregate initialization is
performed.
(3.4) Otherwise, if the initializer list has no elements and T
is a class type with a default constructor, the object is
value-initialized.
(3.5) Otherwise, if T
is a specialization of
std::initializer_list<E>
, the object is constructed as described
below.
(3.6) Otherwise, if T
is a class type, constructors are considered. The applicable constructors are enumerated and the best
one is chosen through overload resolution ([over.match],
[over.match.list]). If a narrowing conversion (see below) is required
to convert any of the arguments, the program is ill-formed.
(3.7) Otherwise, if T
is an enumeration with a fixed underlying type
([dcl.enum]), the initializer-list has a single element v
, and the
initialization is direct-list-initialization, the object is
initialized with the value T(v)
([expr.type.conv]); if a narrowing
conversion is required to convert v
to the underlying type of T
,
the program is ill-formed.
(3.8) Otherwise, if the initializer list has a single element of type E
and either T
is not a reference type or its referenced type is
reference-related to E
, the object or reference is initialized from
that element (by copy-initialization for copy-list-initialization, or
by direct-initialization for direct-list-initialization); if a
narrowing conversion (see below) is required to convert the element to
T
, the program is ill-formed.
(3.9) Otherwise, if T
is a reference type, a prvalue of the type referenced by T
is generated. The prvalue initializes its result
object by copy-list-initialization or direct-list-initialization,
depending on the kind of initialization for the reference. The prvalue
is then used to direct-initialize the reference.
(3.10) Otherwise, if the initializer list has no elements, the object is value-initialized.
(3.11) Otherwise, the program is ill-formed.
(Note: (3.6) is not possible in this case, for the following reason: (3.4) covers the case where a default constructor is present. In order for (3.6) to be considered, a non-default constructor has to be called, which is not possible with an empty initializer list. (3.11) is not possible because (3.10) covers all cases.)
Now let's analyze the cases:
(3.3)
For an aggregate, value-initialization first performs zero-initialization and then, if the element has a non-trivial default constructor, default-initialization, on the aggregate, per [dcl.init]/8:
To value-initialize an object of type T means:
[...]
if T
is a (possibly cv-qualified) class type without a user-provided or deleted default constructor, then the object is
zero-initialized and the semantic constraints for
default-initialization are checked, and if T has a non-trivial default
constructor, the object is default-initialized;
[...]
Non-union aggregates
When copy-initializing a non-union aggregate from {}
, elements that are not explicitly initialized with a default member initializer are copy-initialized from {}
per [dcl.init.aggr]/8:
If there are fewer initializer-clauses in the list than there are
elements in a non-union aggregate, then each element not explicitly
initialized is initialized as follows:
If the element has a default member initializer ([class.mem]), the element is initialized from that initializer.
Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).
Otherwise, the program is ill-formed.
[...]
See Case 1.
Union aggregates
If the aggregate is a union, and no member has a default member initializer, then copying-initializing the aggregate from {}
copy-initializes the first element from {}
: [dcl.init.aggr]/8:
[...]
If the aggregate is a union and the initializer list is empty, then
if any variant member has a default member initializer, that member is initialized from its default member initializer;
otherwise, the first member of the union (if any) is copy-initialized from an empty initializer list.
See Case 1, variant.
(3.4)
Value-initialized, so no difference.
(3.9)
T()
isn't allowed if T
is a reference per [dcl.init]/9:
A program that calls for default-initialization or
value-initialization of an entity of reference type is ill-formed.
See Case 2.
(3.10)
Similarly, value-initialized. No difference.