Logo

Blog


Evaluation order in C++ and uniform initialization

In today's post, you learn or get reminded about an interesting benefit of uniform initialization.

With P0145, C++17 brought us well-defined behavior for various scenarios where, we as users, might have had assumptions about the order of evaluation.

One instance of unspecified order of evaluation I once trapped into during a class is the following:

1
2
3
int i{2};

Fun(++i, ++i);

Even with the latest standard of C++, the order of evaluation here is unspecified. That means each compiler can approach it as it wants. Left-to-right or right-to-left. For me, as I'm used to reading from left to right, the assumption always is that the compiler evaluates the statements in the same way as my natural reading orientation. I know that this is not true for the entire world. A lot of people read right to the left. While writing this post, I wondered how it is for you if you're used to reading right to left. I'm happy to hear your thought on that!

Sadly, as I said, even C++23 leaves room for errors here. Clang and GCC use different evaluation orders at this point.

Calls to constructors

Now to the bright side. Constructors are function calls as well, sort of, at least. That means if I happen to have a class Point3D which takes three parameters and I initialize them like above, I'm looking at unspecified behavior (yes, we also have that in C++):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct Point3D {
  Point3D(int x, int y, int z)
  {
    std::cout << "x:" << x << " y:" << y << " z:" << z << '\n';
  }
};

void Use()
{
  int     dubiouseInit{2};
  Point3D pt{++dubiouseInit, ++dubiouseInit, ++dubiouseInit};
}

Oh right, I was talking about a bright side. Well, since C++11, we have uniform initialization using curly braces. They come with a well-defined order of evaluation, always left to right. Aside from preventing narrowing conversions, uniform initialization also gives us a defined order of evaluation.

The guarantee even holds when you pass a temporary, say, a Point3D object to a function and initialize the object using braced initialization.

1
2
3
4
5
6
7
void Fun(const Point3D pt);

void UseFun()
{
  int i{2};
  Fun(Point3D{i, ++i, ++i});
}

While I recommend that, in general, you should not rely on the order of evaluation, the guarantee for the evaluation order the uniform initialization gives us, in my experience, is an often overlooked benefit.

Andreas