Tuesday, 6 December 2016

Two interesting quirks of C++ uniform initialization


We all know and love the new C++11's uniform initialization feature (aka curly braces initialization), and, to be frank, there is much to warrant this love, like:

with structs
  struct X { bool x{false}; int i; ...};
  X x = { true, 100, ... };
  X x{ true, 100, ... };
with arrays:
  struct Y { int arr[3], bool b, ...};
  Y y{ {0,1,2}, false, ... };
  int* intArray = new int[3]{1, ,2, 3};
with library classes:
  std::vector<int> a = { 1, 2, 4 }; // yesss! At last!
  QMap<QString, QVector<int>> a = { {"0x111", { 1, 1, 1} }, {"0x100", { 1, 0, 0} } };
and you can enable it for you own classes as well, writing a constructor taking std::initializer_list as argument (example missing, but you know what I mean...)!

because it's universal, you can use it for types too:
  int i{1};
  int j{};
but it's a bit redundant here, as we already could do:
  int i(1);
  int j(0); // not j()! I never used () but assumed it to be the default initialized int :(
but there are 2 additional goodies packed into that, as I learned recently, namely:

1. The first one
this is an old problem: unexpectedly, the compiler will consider this:
  TimeKeeper time_keeper();
not to be an object instantiation but a function definition! Here TimeKeeper is a class (reused) from the Wikipedia article:
  class TimeKeeper {
    public:
      TimeKeeper();
      int get_time();
  };
This the compiler will balk at:
  int t = time_keeper.get_time();
But thanks to uniform initialization not at this:
  TimeKeeper time_keeper1{};
  int t = time_keeper1.get_time();
OK, you are right, that's not the most vexing parse 😉, but simply incorrect usage of the constructor! The most vexing parse requires a parameter to the constructor! But I made this error several times myself when blindly typing ahead...  none the less, the problem is the same, only with a parameter:
  TimeKeeper time_keeper(Timer());
Here is a function with taking a function(!) like Timer mkTimer() as single, unnamed (!) parameter. Vexing? Now correct that with a single stroke (or two):
  TimeKeeper time_keeper{Timer()};
Nice to know when you need a workaround for a vexing parse!

2. The second one
Here Bjarne himself explains that:
  int x = 7.3; // Ouch!
but
  int x0 {7.3}; // error: narrowing
  int x1 = {7.3}; // error: narrowing
Moreover, compiler will automatically check int sizes on initializing (in Bjarne's words again):
  char c1{7}; // OK: 7 is an int, but it fits in a char  
  char c2{77777}; // error: narrowing (assuming 8-bit chars)
That's nice.

Considered I am a traditionalist and like my code to look like a old, regular C++, but these features make a nice argument in favor of using curly braces instead of the normal ones! Will for sure consider that!


3 comments:

Anonymous said...

Actually

int j{}; // (1)

is not the same as:

int j(); // (2)

Since (2) is also an example of the "most vexing parse" you're talking about later in your post.

Try:

int j();
j = 2;

Marek Krj said...

@Anonymous - vexing, vexing, from my old days I remembered this to be a default initialized (i.e. zeroed) integer. Will check that. Thanks!

Francisco Lopes said...

This is one situation I find it neat:

std::ifstream source("myfile.dat", std::ios::binary);
std::vector data(std::istreambuf_iterator(source), {});

For context of this code check:

http://stackoverflow.com/questions/4423361/constructing-a-vector-with-istream-iterators

My answer using this is listed there, but it's not the accepted one.