Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++11 uniform initialization: ambiguity between initializer list and multiple-parameter constructors?

Tags:

c++

c++11

Currently trying to wrap my head around C++11's uniform initialization. I came upon this ambiguous case: consider a class which can either be constructed from either a two-argument constructor or an initializer list of any length:

class Foo {
  public:
    Foo(int a, int b) {
      std::cout << "constructor 1" << std::endl;
    }
    Foo(std::initializer_list<int>) {
      std::cout << "constructor 2" << std::endl;
    }
};

Following uniform initialization convention, I'd expect the following to work:

Foo a (1, 2) prints constructor 1 (duh)

Foo b {1, 2} prints constructor 1

Foo c = {1, 2} prints constructor 2

However, it seems like the compiler interprets Foo b {1, 2} as a list initialization, and calls constructor 2. Is the () syntax the only way to force the compiler to consider other kinds of constructors when an initializer-list constructor is present?

like image 557
Timothy Kanarsky Avatar asked Oct 01 '20 00:10

Timothy Kanarsky


Video Answer


2 Answers

it seems like the compiler interprets Foo b {1, 2} as a list initialization, and calls constructor 2. Is the () syntax the only way to force the compiler to consider other kinds of constructors when an initializer-list constructor is present?

Quotes from standard draft explains this well:

9.4.5.2 [dcl.init.list] (emphasis mine):

A constructor is an initializer-list constructor if its first parameter is of type std​::​initializer_­list or reference to cv std​::​initializer_­list for some type E, and either there are no other parameters or else all other parameters have default arguments ([dcl.fct.default]).

[Note 2: Initializer-list constructors are favored over other constructors in list-initialization ([over.match.list]). Passing an initializer list as the argument to the constructor template template C(T) of a class C does not create an initializer-list constructor, because an initializer list argument causes the corresponding parameter to be a non-deduced context ([temp.deduct.call]). — end note]

and 12.4.2.8 [over.match.list]:

When objects of non-aggregate class type T are list-initialized such that [dcl.init.list] specifies that overload resolution is performed according to the rules in this subclause or when forming a list-initialization sequence according to [over.ics.list], overload resolution selects the constructor in two phases:

  • If the initializer list is not empty or T has no default constructor, overload resolution is first performed where the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T and the argument list consists of the initializer list as a single argument.

  • Otherwise, or if no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

like image 74
Andreas DM Avatar answered Oct 05 '22 15:10

Andreas DM


You can add an extra ignored argument to your constructor to specify a particular overload at callsite, like they do in STL:

#include <iostream>

struct non_init_list_t {};
inline constexpr non_init_list_t non_init_list;

struct Class {
    Class(int a, int b, non_init_list_t = non_init_list) { std::clog << "()\n"; }
    Class(std::initializer_list<int> list) { std::clog << "{}\n"; }
};

Class a{12, 42, non_init_list};  // ()
Class b{12, 42};                 // {}
Class c(12, 42);                 // ()
like image 43
bipll Avatar answered Oct 05 '22 14:10

bipll