List, Aggregate, and Designated Initialization

A quick guide to creating objects using lists, including std::initializer_list, aggregate and designated initialization
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

At this point, we’re likely very familiar with initializing objects by specifying the type of object we want to construct, and a list of values to pass to a constructor defined as part of that type.

Typically, this looks something like this:

#include <iostream>

struct Vec3 {
  // Constructor using a member initializer list
  Vec3(int x, int y, int z)
    : x{x}, y{y}, z{z}
  {}

  int x{0};
  int y{0};
  int z{0};

  void Log(){
    std::cout
      << "x=" << x
      << ", y=" << y
      << ", z=" << z << '\n';
  }
};

int main(){
  // Calling the constructor to create an object
  Vec3 Vector{1, 2, 3};
  Vector.Log();
}
x=1, y=2, z=3

This is referred to as list initialization, and the previous example is only one way to use it.

In this lesson, we’ll cover some of the other useful scenarios. We’ll also introduce other forms of list initialization, including aggregate initialization, and designated initialization.

List Initialization during Assignment

When we want to construct a new object to assign to a variable, we don’t need to specify the type of the new object. After all, it will inherently have the same type as the variable we’re storing it in.

This is clearly true when the type can be initialized from a single value:

int Number { 1 };

// We can include the type during assignment...
Number = int{2};

// ...but we don't need to
Number = 3;

The same applies to any type. If we need multiple constructor arguments to create the new object we’re assigning to our variable, we just need to provide them as a list:

#include <iostream>

struct Vec3 {/*...*/}; int main(){ Vec3 Vector; Vector = {1, 2, 3}; Vector.Log(); }
x=1, y=2, z=3

List Initialization with Functions

Similar shortcuts are available when constructing an object to return immediately. In scenarios like this, we need to provide a list of constructor arguments.

The compiler knows what type we’re constructing - it will be the return type of the function:

#include <iostream>

struct Vec3 {/*...*/}; Vec3 GetVector(){ return {1, 2, 3}; } int main(){ GetVector().Log(); }
x=1, y=2, z=3

The same logic applies when constructing an object to pass to a function. We only need to pass the constructor arguments - the type can be inferred from the function’s parameter list:

#include <iostream>

struct Vec3 {/*...*/}; void HandleVector(Vec3 V){ V.Log(); } int main(){ HandleVector({1, 2, 3}); }
x=1, y=2, z=3

List Initialization within Constructors

As a final example, we can also provide a simple list of arguments to construct an object within the member initializer list of another constructor.

Below, our Character constructor is constructing a Vec3 using some of its parameters. The constructor doesn’t need to specify the type it’s constructing - just the variable that will be used.

The compiler knows the variable - Position, in this case, is a Vec3 - so it understands how to use the list:

#include <iostream>

struct Vec3 {/*...*/}; struct Character { Character( std::string Name, int x, int y, int z) : Name{Name}, Position{x, y, z} {} std::string Name; Vec3 Position; }; int main(){ Character Player("Anna", 1, 2, 3); std::cout << Player.Name << " Position: "; Player.Position.Log(); }
Anna Position: x=1, y=2, z=3

We covered member initializer lists in more detail in the introductory course:

Storing Initialization Lists using std::initializer_list

Sometimes, we need to store initializer lists as standalone objects. The std::initializer_list template class is designed for this. It accepts a single template parameter, representing the type of values we’re storing in the list.

#include <iostream>

void Log(std::initializer_list<int> Numbers){
  for (auto x : Numbers) {
    std::cout << x << ", ";
  }
}

int main(){
  Log({1, 2, 3, 4, 5});
}
1, 2, 3, 4, 5,

Often, std::initializer_lists are used in scenarios where we need to forward arguments from one constructor to another.

Below, we’ve laid the basic foundations of a custom container type. This container accepts a list of initial values, which allows it to be initialized using an API that will be familiar:

CustomContainer Numbers{1, 2, 3, 4, 5};

Below, our CustomContainer type captures initialization values as a std::initializer_list within a constructor. This is so the initial collection can be forwarded to a private std::vector, which manages our type's underlying storage:

#include <iostream>
#include <vector>

template <typename T>
class CustomContainer {
public:
  CustomContainer(
    std::initializer_list<T> Contents) :
    Container{Contents}{}

  auto begin() const{
    return Container.begin();
  }

  auto end() const{ return Container.end(); }

private:
  std::vector<T> Container;
};

int main(){
  CustomContainer Numbers{1, 2, 3, 4, 5};

  for (int x : Numbers) {
    std::cout << x << ", ";
  }
}
1, 2, 3, 4, 5,

A std::initializer_list is a lightweight, read-only wrapper. As such, it is fast to copy, so generally passed by value rather than by reference.

std::initializer_list is also a homogenous container. That is, every value it contains must be the same type.

A container that can store a variety of value types is referred to as heterogenous. Standard library support for these types of containers is currently limited. The closest heterogeneous equivalent to a std::initializer_list is std::tuple, which we covered in an earlier lesson:

Aggregate Initialization

Previously, we’ve seen when we’re working with a simple type, we can initialize objects simply by providing values for the data members. Our Vec3 struct can be initialized from a list of 3 int objects, even though it has no such constructor defined:

#include <iostream>

struct Vec3 {/*...*/}; int main(){ Vec3 V{1, 2, 3}; V.Log(); }

In this example, our Vec3 struct had declared public data members x, y, and z - in that order, and we created an object using a list of 1, 2, and 3 - in that order.

Thus, our object is initialized with those values, and our program output is:

x=1, y=2, z=3

This is referred to as aggregate initialization. It only works with aggregates.

Aggregates are simple structs, classes, and unions that do not use some of the more advanced features. Specifically, as of C++23, for our type to be an aggregate, it can not have:

  • any user-defined constructors
  • any non-public variables, unless they’re inherited
  • any virtual functions
  • any private, protected, or virtual base classes

If our type meets this criterion, it is an aggregate, and its objects can be initialized using aggregate initialization.

Designated Initialization

As of C++20, we can initialize aggregates using designated initializers. Alongside each value, we provide the name of the field we’re initializing, prefixed by a period. Using designated initializers, our previous example would look like this:

#include <iostream>

struct Vec3 {/*...*/}; int main(){ Vec3 V{.x = 1, .y = 2, .z = 3}; V.Log(); }
x=1, y=2, z=3

Within a designated initializer, the fields do not need to be in the same order as defined within our type. The main advantage is they allow us to omit some of the fields from the list.

Below, we use a designated initializer to define values for x and z, omitting y. Therefore, y takes on the default value defined in the struct declaration:

#include <iostream>

struct Vec3 {/*...*/}; int main(){ Vec3 V{.x = 1, .z = 3}; V.Log(); }
x=1, y=0, z=3

Designated Initializers in C

The concept of designated initialization also existed in the C language, using a similar syntax. It was only added to C++ in the recent C++20 spec.

Older resources that document designated initializers are likely to reference how they were used in C. The C++ implementation we've introduced here works slightly differently.

Summary

In this lesson, we explored various initialization techniques, including list, aggregate, and designated initialization.

Main Points Learned:

  • List initialization, including its use in object construction and assignment.
  • How to use aggregate initialization to instantiate objects of simple types.
  • The introduction and advantages of designated initialization in C++20, allowing for more readable and selective assignment of object members.
  • Practical applications of these initialization techniques in various contexts, such as function returns, parameter passing, and within constructors
  • The role of std::initializer_list for capturing and forwarding lists of homogenous values.

Was this lesson useful?

Next Lesson

User Defined Conversions

Learn how to add conversion functions to our classes, so our custom objects can be converted to other types.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

List, Aggregate, and Designated Initialization

A quick guide to creating objects using lists, including std::initializer_list, aggregate and designated initialization

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Objects, Classes and Modules
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

User Defined Conversions

Learn how to add conversion functions to our classes, so our custom objects can be converted to other types.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved