C++14 conformance improvements: constexpr and aggregate initialization

Two important features in C++11 received small upgrades in C++14 that had far-reaching effects.

  1. In C++11, constexpr function bodies were allowed only to be a single return statement. In C++14, nearly all statements are now allowed in constexpr functions, allowing for much more idiomatic C++. You can learn more about what’s allowed in core constant expressions on CppReference as well as learn about constexpr on the same site.
  2. An aggregate type in C++11 could not be initialized as an aggregate if it contained any members that had default member initializers. In C++14 the committee removed this restriction on non-static data members having default member initializers. You can find out more about aggregate initialization on CppReference as well as about non-static data member initialization (known as NSDMI.)

You can see from the CppReference site that while both of these changes were small changes to the standard (search for the “C++14” annotation on those pages) they had broad impact on the compiler’s implementation and on what code developers can write.

  • Previously, constexpr was basically all about expressions, but C++14 extended it to allow control flow statements. The difference in what you can write in your code is huge: now instead of just being able to write expressions you can create idiomatic compile-time value compilations and mutation.
  • The changes to allow aggregate initialization for aggregates that contain non-static data members makes a useful case of aggregate initialization possible.

We’re happy to announce that the compiler shipping with Visual Studio 2017 supports both extended constexpr and NSDMI for aggregates fully.

Extended constexpr

Our extended constexpr work was test-driven. We tested against a number of libraries that are heavy users of extended constexpr, including:

  • Sprout
  • Boost (with MSVC workarounds removed)
  • Boost Hana
  • Guidelines Support Library (GSL)
  • libc++ tests
  • Microsoft’s Range-v3 fork
  • C++ Standard Libraries

We’ve also evaluated against test suites from McCluskey, Plumhall, and Perennial, as well as a test suite built from code snippets you sent us from an online survey, online forums, and early bug reports from the VC Blog.

Taking advantage of C++14 constexpr is almost as easy as putting the constexpr keyword on existing functions, because extended constexpr allows a developer to use idiomatic C++ for their constexpr functions.

  • Local variable declarations and conditionals can now be used in constexpr functions. [see example 1 below]
  • Instead of recursion, all looping constructs (except goto) are allowed in constexpr functions [see example 2 below]

Using iteration instead of recursion should perform better at compile time, and, as constexpr functions can be used at runtime, will perform better at runtime as well. We’ve beefed up our constexpr control flow engine to eagerly identify cases where a constexpr function is guaranteed to evaluate illegal expressions, so a class of errors can be caught at constexpr-authoring time rather than after a library has been shipped to developers.

constexpr examples

// Example 1a
enum class Messages { 
	Hi, 
	Goodbye 
};

constexpr Messages switch_with_declaration_in_control(int x)
{
    switch (int value = x)
    {
        case 0:                 return Messages::Hi;
        default: return Messages::Goodbye;
    }
}


// Example 1b
constexpr bool even(int x) {
	if (x % 2 == 0)
		return true;
	return false;
}

// Example 2a
constexpr int simple_count_up_do_loop(int x) {
	int i = 0;
	do	{
		++i;
	} while (i < x);
	return i;
}

// Example 2b
constexpr int multiple_initializers_multiple_incr_count_up(int x) {
	int higher = 0;
	for (auto i = 0, j = 1; i <= x; ++i, j++)
		higher = j;
	return higher;
}

// Negative test cases
constexpr bool always(int x) {
	return x;
}

constexpr int guaranteed_to_hit_true_path() {
	if (always(1))
		throw "OH NO"; // illegal expression, guaranteed to be hit
	return 0;
}

constexpr int guaranteed_to_hit_false_path() {
	if (42 * 2 - 84)
		return 1;
	else
		throw "OH NO"; // illegal expression, guaranteed to be hit
	return 0;
}

constexpr int guaranteed_to_evaluate_while_loop() {
	while (always(33)) {
		new int(0);    // illegal expression, guaranteed to be hit
	}
	return 0;
}

constexpr int guaranteed_to_evaluate_for_loop() {
	for (; always(22); )
		new int();     // illegal expression, guaranteed to be hit
	return 0;
}

NSDMI for aggregates

The NSDMI for aggregates changes were more limited in scope but automatically improve a lot of existing code. In C++ 11, the presence of a default member initializer (NSDMI) would preclude the class from being an aggregate; therefore, it would not be eligible for aggregate initialization.

An example:

 
struct S {
  int i = 1;
  int j = 2;
};
 
S s1;   // OK; calls the default constructor, which initializes 'i' and 'j' to 1 and 2.
S s2{}; // OK; not aggregate initialization because S is not an aggregate; calls the default constructor.
s3{42}; // Error; S is not an aggregate and there is no appropriate constructor.

In C++14, S is now considered to be an aggregate class type, so aggregate initialization can be used:

 
S s4{}; // OK with C++14; aggregate initialization; no initializer is provided for 'i' or 'j', so their respective default member initializers will be used to initialize them to 1 and 2.
S s5{42}; // OK with C++14; aggregate initialization; 'i' is explicitly initialized to 42 and no initializer is provided for 'j', so its default member initializer will be used to initialize it to 2.

In closing

As always, we welcome your feedback. Feel free to send any comments through e-mail at visualcpp@microsoft.com, through Twitter @visualc, or Facebook at Microsoft Visual Cpp.

If you encounter other problems with Visual C++ in VS 2017 please let us know via the Report a Problem option, either from the installer or the Visual Studio IDE itself. For suggestions, let us know through UserVoice. Thank you!