Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 2 – Types, values and expressions

Variables and initialisation: =, () and {}

Declare C++ variables with =, () and {}, see why braces reject narrowing conversions, and stop reading variables that never got a value.

  • Beginner
  • 18 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 18 minutes

What you will learn

  • Declare and initialise variables in the three common forms
  • Explain why brace initialisation rejects narrowing
  • Avoid reading variables that were never initialised

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare and initialise variables in the three common forms
  • Explain why brace initialisation rejects narrowing
  • Avoid reading variables that were never initialised

Before you start

On this page

A variable in C++ is a named object of a fixed type, and its declaration can give it a first value in three ways: int a = 1;, int b(2); and int c{3};. For a plain number all three store the value, so the choice looks like a matter of taste. The differences show at the edges. Braces refuse a conversion that would lose information, so int d{2.5}; does not compile, while int e = 2.5; compiles and stores 2. Empty parentheses, as in std::string s();, declare a function instead of a string. And a local variable declared with no initialiser, int n;, starts with no defined value at all: reading it is undefined behaviour in C++23. This lesson runs each case through two compilers, Apple clang 21 and GCC 16, and ends with one habit that avoids all of them: give every variable its value where you declare it, and use braces when you want the compiler to check the conversion.

Three ways to write the first value

A declaration names a type, then the variable, then its initialiser, the expression that gives the first value. This program uses every form:

three_forms.cpp C++ · three_forms.cpp
#include <print>
#include <string>

int main() {
    int apples = 4;              // copy-initialisation: = and a value
    int pears(7);                // direct-initialisation: the value in parentheses
    int plums{9};                // list-initialisation: the value in braces
    int figs = {2};              // copy-list-initialisation: = and braces
    double price{12.5};
    std::string city("Pune");
    std::string fruit{"mango"};

    std::println("{} {} {} {}", apples, pears, plums, figs);
    std::println("{} {} {}", price, city, fruit);
}
Build and run it with both compilers Python · three_forms_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra three_forms.cpp -o three_forms
./three_forms
g++-16 -std=c++23 -Wall -Wextra three_forms.cpp -o three_forms
./three_forms
""")

Output

$ clang++ -std=c++23 -Wall -Wextra three_forms.cpp -o three_forms
$ ./three_forms
4 7 9 2
12.5 Pune mango
$ g++-16 -std=c++23 -Wall -Wextra three_forms.cpp -o three_forms
$ ./three_forms
4 7 9 2
12.5 Pune mango

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 three_forms_session.py

Each session on this page shows the commands as you would type them in a terminal, followed by what they printed. Here both compilers build the program without a single warning, even with -Wall -Wextra switched on, and it prints the same two lines. The standard names the forms after their syntax:

Written as Name in the standard
int apples = 4; copy-initialisation
int pears(7); direct-initialisation
int plums{9}; direct-list-initialisation
int figs = {2}; copy-list-initialisation

“Copy” here describes the syntax, not the work done: int apples = 4; copies nothing, it stores 4. For a class type the forms can choose different constructors, which the last sections of this lesson show. The two forms with braces, together called list-initialisation, add one check that the others lack, and that check is the main reason style guides recommend them.

Braces reject narrowing conversions

A narrowing conversion is one that can lose information. The standard lists them: a floating-point value to an integer type; a double to a float; an integer to a floating-point type; and an integer to an integer type that cannot hold all of its values, such as long long to int or int to unsigned. Copy-initialisation and direct-initialisation perform all of these without complaint. List-initialisation makes the program ill-formed:

narrowing.cpp C++ · narrowing.cpp
#include <print>

int main() {
    double price = 2.5;
    int a = price;   // copy-initialisation converts silently: the .5 is dropped
    int b(price);    // direct-initialisation does the same
    int c{price};    // braces: a narrowing conversion, which the language forbids
    std::println("a = {}, b = {}, c = {}", a, b, c);
}
Narrowing with Apple clang and with GCC Python · narrowing_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra narrowing.cpp -o narrowing
g++-16 -std=c++23 -Wall -Wextra narrowing.cpp -o narrowing
./narrowing
g++-16 -std=c++23 -Wall -Wextra -pedantic-errors narrowing.cpp -o narrowing
""")

Output

$ clang++ -std=c++23 -Wall -Wextra narrowing.cpp -o narrowing
narrowing.cpp:7:11: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
    7 |     int c{price};    // braces: a narrowing conversion, which the language forbids
      |           ^~~~~
narrowing.cpp:7:11: note: insert an explicit cast to silence this issue
    7 |     int c{price};    // braces: a narrowing conversion, which the language forbids
      |           ^~~~~
      |           static_cast<int>( )
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra narrowing.cpp -o narrowing
narrowing.cpp: In function 'int main()':
narrowing.cpp:7:11: warning: narrowing conversion of 'price' from 'double' to 'int' [-Wnarrowing]
    7 |     int c{price};    // braces: a narrowing conversion, which the language forbids
      |           ^~~~~
$ ./narrowing
a = 2, b = 2, c = 2
$ g++-16 -std=c++23 -Wall -Wextra -pedantic-errors narrowing.cpp -o narrowing
narrowing.cpp: In function 'int main()':
narrowing.cpp:7:11: error: narrowing conversion of 'price' from 'double' to 'int' [-Wnarrowing]
    7 |     int c{price};    // braces: a narrowing conversion, which the language forbids
      |           ^~~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 narrowing_session.py

Apple clang refuses to build the file. It points at the braces in line 7 and suggests the fix itself: write static_cast<int>(price) if a conversion is what you mean. GCC only warns, builds the program, and the program prints a = 2, b = 2, c = 2. That is not a gap in the rule. The standard says the program is ill-formed, and GCC’s manual describes its own choice: narrowing from a constant is an error, while narrowing from a value that is not a constant only produces a warning, so that older code keeps building. With -pedantic-errors, the last command, GCC refuses as well. Notice also what neither compiler said: lines 5 and 6 drop the .5 with no warning at all. Only -Wconversion, which neither -Wall nor -Wextra turns on, reports them.

Which conversions count as narrowing is decided by the types, not by whether the value happens to fit. This program asks the compiler about pairs of types, using a requires expression that is true when To{value} compiles for a value of type From that the compiler cannot know in advance:

narrowing_rules.cpp C++ · narrowing_rules.cpp
#include <print>

// braces_accept<To, From> is true when To{value} compiles for a value of type From
// that is not a constant: the compiler cannot see what the value will be.
template <class To, class From>
constexpr bool braces_accept = requires(From value) { To{value}; };

constexpr const char* verdict(bool ok) { return ok ? "compiles" : "rejected: narrowing"; }

int main() {
    std::println("long long{{int}}   {}", verdict(braces_accept<long long, int>));
    std::println("double{{float}}    {}", verdict(braces_accept<double, float>));
    std::println("int{{double}}      {}", verdict(braces_accept<int, double>));
    std::println("float{{double}}    {}", verdict(braces_accept<float, double>));
    std::println("double{{int}}      {}", verdict(braces_accept<double, int>));
    std::println("int{{long long}}   {}", verdict(braces_accept<int, long long>));
    std::println("unsigned{{int}}    {}", verdict(braces_accept<unsigned, int>));
    std::println("char{{int}}        {}", verdict(braces_accept<char, int>));
    std::println("bool{{int}}        {}", verdict(braces_accept<bool, int>));

    // Constants are checked by value: these fit, so they compile.
    char letter{65};
    float ratio{0.1};
    double whole{7};
    std::println("char{{65}} = {}, float{{0.1}} = {}, double{{7}} = {}", letter, ratio, whole);
}
Which brace conversions compile Python · narrowing_rules_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra narrowing_rules.cpp -o narrowing_rules
./narrowing_rules
g++-16 -std=c++23 -Wall -Wextra narrowing_rules.cpp -o narrowing_rules
./narrowing_rules
""")

Output

$ clang++ -std=c++23 -Wall -Wextra narrowing_rules.cpp -o narrowing_rules
$ ./narrowing_rules
long long{int}   compiles
double{float}    compiles
int{double}      rejected: narrowing
float{double}    rejected: narrowing
double{int}      rejected: narrowing
int{long long}   rejected: narrowing
unsigned{int}    rejected: narrowing
char{int}        rejected: narrowing
bool{int}        rejected: narrowing
char{65} = A, float{0.1} = 0.1, double{7} = 7
$ g++-16 -std=c++23 -Wall -Wextra narrowing_rules.cpp -o narrowing_rules
$ ./narrowing_rules
long long{int}   compiles
double{float}    compiles
int{double}      rejected: narrowing
float{double}    rejected: narrowing
double{int}      rejected: narrowing
int{long long}   rejected: narrowing
unsigned{int}    rejected: narrowing
char{int}        rejected: narrowing
bool{int}        rejected: narrowing
char{65} = A, float{0.1} = 0.1, double{7} = 7

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 narrowing_rules_session.py

The two compilers agree on every line; inside a requires expression GCC treats narrowing as an error too. Widening conversions such as long long{int} and double{float} are fine. double{int} is rejected although every 32-bit int fits exactly in a double: the rule says an integer type to a floating-point type is narrowing, whatever the sizes. Constants are different, because the compiler can see the value. char{65}, float{0.1} and double{7} compile, since each value fits its target, while char{300}, unsigned{-1} and int{2.0} do not; a floating-point value to an integer type is narrowing even when it is a whole number.

When you do want a lossy conversion, say so. static_cast<int>(price) truncates towards zero, and std::lround(price) from <cmath> rounds to the nearest whole number, halfway cases away from zero, so 2.5 becomes 3. Either one tells the next reader that losing the fraction was the plan.

A variable without an initialiser

Four declarations of int n and the value each starts with: 5, 0, an indeterminate value inside a function, and 0 outside one.int n = 5;int n(5);int n{5};Starts as 5;braces alsocheck narrowingint n{};Starts as 0int n;inside afunctionNo defined value;reading it isundefined (C++26:erroneous)int n; outsideany function,or static int n;Starts as 0,before main()runs

What value int n starts with

Text description of the diagram

The diagram has four rows. Each row shows a way to declare int n on the left and, after an arrow, the value n starts with on the right.

  1. int n = 5;, int n(5); or int n{5}; start as 5. The braces form also checks the conversion for narrowing.
  2. int n{}; starts as 0.
  3. int n; inside a function has an indeterminate value: reading it is undefined behaviour, and erroneous behaviour in C++26.
  4. int n; outside any function, or static int n;, starts as 0, before main() runs.

When a declaration has no initialiser, the variable is default-initialised. For a class type that runs its default constructor: std::string city; is an empty string. For int, double, a pointer, or a struct made only of such members, default-initialisation does nothing, and a variable declared inside a function holds an indeterminate value: whatever bits that memory held before. Reading an indeterminate value is undefined behaviour in C++23. The compiler may assume that a correct program never does it, so the symptom can change with the compiler, the optimisation level or an unrelated edit nearby. This function forgets to start its total at zero:

uninitialised.cpp C++ · uninitialised.cpp
#include <print>

int total_of(const int* prices, int count) {
    int total;                           // no initialiser: the bug
    for (int i = 0; i < count; ++i) {
        total += prices[i];
    }
    return total;
}

int main() {
    const int prices[] = {120, 80, 45};
    std::println("total: {}", total_of(prices, 3));
}
Who notices the missing initialiser Python · uninitialised_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -c uninitialised.cpp
g++-16 -std=c++23 -Wall -Wextra -c uninitialised.cpp
g++-16 -std=c++23 -Wall -Wextra -O2 -c uninitialised.cpp
g++-16 -std=c++26 -Wall -Wextra uninitialised.cpp -o uninitialised
./uninitialised
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -c uninitialised.cpp
uninitialised.cpp:6:9: warning: variable 'total' is uninitialized when used here [-Wuninitialized]
    6 |         total += prices[i];
      |         ^~~~~
uninitialised.cpp:4:14: note: initialize the variable 'total' to silence this warning
    4 |     int total;                           // no initialiser: the bug
      |              ^
      |               = 0
1 warning generated.
$ g++-16 -std=c++23 -Wall -Wextra -c uninitialised.cpp
$ g++-16 -std=c++23 -Wall -Wextra -O2 -c uninitialised.cpp
uninitialised.cpp: In function 'int total_of(const int*, int)':
uninitialised.cpp:6:15: warning: 'total' may be used uninitialized [-Wmaybe-uninitialized]
    6 |         total += prices[i];
      |         ~~~~~~^~~~~~~~~~~~
uninitialised.cpp:4:9: note: 'total' was declared here
    4 |     int total;                           // no initialiser: the bug
      |         ^~~~~
In function 'int total_of(const int*, int)',
    inlined from 'int main()' at uninitialised.cpp:13:39:
uninitialised.cpp:6:15: warning: 'total' is used uninitialized [-Wuninitialized]
    6 |         total += prices[i];
      |         ~~~~~~^~~~~~~~~~~~
uninitialised.cpp: In function 'int main()':
uninitialised.cpp:4:9: note: 'total' was declared here
    4 |     int total;                           // no initialiser: the bug
      |         ^~~~~
$ g++-16 -std=c++26 -Wall -Wextra uninitialised.cpp -o uninitialised
$ ./uninitialised
total: 245

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 uninitialised_session.py

The session shows three different answers to one bug:

  • Apple clang warns as soon as it compiles the file, without any optimisation, and even suggests = 0.
  • GCC says nothing at the default level, -O0. Its uninitialised-variable warnings come from analysis done while optimising, and its manual notes that which variables are flagged depends on the optimisation options. With -O2 the same file produces two warnings. Building with optimisation now and then catches warnings like these.
  • GCC 16 in C++26 mode prints total: 245, the right answer, by luck.

C++26 changes the rules for this mistake. Proposal P2795R5 makes reading an uninitialised local variable erroneous behaviour instead of undefined behaviour: the variable holds a value that the implementation chooses, the program goes on in a predictable way, and the compiler is encouraged to diagnose the read. The code is still a bug, and the standard says so. GCC 16 implements it: in C++26 mode it fills such variables with zeros unless -ftrivial-auto-var-init says otherwise, and the attribute [[indeterminate]] on a variable opts out where speed matters. Apple clang 21 does not implement erroneous behaviour yet, and neither does upstream Clang, whose status page lists P2795R5 as not supported.

Two related facts complete the picture. A variable declared outside any function, or declared static, is zero-initialised before main starts, so int counter; at namespace scope is 0; the rule follows where the variable lives, and relying on it hides intent. And at run time, Clang’s MemorySanitizer (-fsanitize=memory) detects reads of uninitialised memory, but it runs only on Linux, NetBSD and FreeBSD, not on macOS. The reliable fix is the initialiser: int total{}; or int total = 0;.

Empty parentheses declare a function

vexing_parse.cpp C++ · vexing_parse.cpp
#include <print>
#include <string>

int main() {
    std::string name();     // meant as an empty string: declares a function instead
    std::string title{};    // an empty string
    std::string city;       // also an empty string: std::string has a default constructor
    std::println("title: [{}], city: [{}]", title, city);
}
A declaration that is not a variable Python · vexing_parse_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra vexing_parse.cpp -o vexing_parse
./vexing_parse
g++-16 -std=c++23 -Wall -Wextra vexing_parse.cpp -o vexing_parse
./vexing_parse
""")

Output

$ clang++ -std=c++23 -Wall -Wextra vexing_parse.cpp -o vexing_parse
vexing_parse.cpp:5:21: warning: empty parentheses interpreted as a function declaration [-Wvexing-parse]
    5 |     std::string name();     // meant as an empty string: declares a function instead
      |                     ^~
vexing_parse.cpp:5:21: note: remove parentheses to declare a variable
    5 |     std::string name();     // meant as an empty string: declares a function instead
      |                     ^~
1 warning generated.
$ ./vexing_parse
title: [], city: []
$ g++-16 -std=c++23 -Wall -Wextra vexing_parse.cpp -o vexing_parse
vexing_parse.cpp: In function 'int main()':
vexing_parse.cpp:5:21: warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse]
    5 |     std::string name();     // meant as an empty string: declares a function instead
      |                     ^~
vexing_parse.cpp:5:21: note: remove parentheses to default-initialize a variable
    5 |     std::string name();     // meant as an empty string: declares a function instead
      |                     ^~
      |                     --
$ ./vexing_parse
title: [], city: []

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vexing_parse_session.py

std::string name(); reads like “a string called name, made with no arguments”, but C++ treats anything that can be a declaration as one, and this line can declare a function called name that takes no arguments and returns a std::string. The rule is nicknamed the most vexing parse. Both compilers warn about it by default, and the moment you use name as a string you get an error that seems to have nothing to do with the line that caused it. Write std::string name; or std::string name{}; instead. A pair of braces can never be read as a parameter list, which is the second reason braces are recommended.

Braces with auto, and with constructors

auto gives a variable the type of its initialiser, and braces interact with it in a way worth knowing. Since C++17, which adopted proposal N3922, auto b{5}; makes an int. But auto c = {5};, with the equals sign, still makes a std::initializer_list<int>, a list holding one value, and auto e{1, 2}; does not compile. The usual style is therefore auto count = 5;.

Braces also prefer a constructor that takes a std::initializer_list whenever the class has one, and the standard containers do. For a std::vector, the parentheses in (3, 7) mean “three elements, each 7”, while the braces in {3, 7} mean “the elements 3 and 7”:

Braces with auto and with std::vector

auto_braces.cpp

#include <initializer_list>
#include <print>
#include <type_traits>

int main() {
    auto a = 5;          // int
    auto b{5};           // int as well (since C++17)
    auto c = {5};        // std::initializer_list<int>: a list holding one int
    auto d = {1, 2, 3};  // std::initializer_list<int> with three values

    static_assert(std::is_same_v<decltype(a), int>);
    static_assert(std::is_same_v<decltype(b), int>);
    static_assert(std::is_same_v<decltype(c), std::initializer_list<int>>);

    std::println("a = {}, b = {}, c holds {} value, d holds {} values", a, b, c.size(), d.size());
}

vector_forms.cpp

#include <print>
#include <vector>

int main() {
    std::vector<int> a(3, 7);   // parentheses: 3 elements, each 7
    std::vector<int> b{3, 7};   // braces: the list 3, 7
    std::vector<int> c(3);      // 3 elements, each 0
    std::vector<int> d{3};      // the list 3
    std::println("a = {}", a);
    std::println("b = {}", b);
    std::println("c = {}", c);
    std::println("d = {}", d);
}
Both files on both compilers Python · braces_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra auto_braces.cpp -o auto_braces
./auto_braces
clang++ -std=c++23 -Wall -Wextra vector_forms.cpp -o vector_forms
./vector_forms
g++-16 -std=c++23 -Wall -Wextra auto_braces.cpp -o auto_braces
./auto_braces
g++-16 -std=c++23 -Wall -Wextra vector_forms.cpp -o vector_forms
./vector_forms
""")

Output

$ clang++ -std=c++23 -Wall -Wextra auto_braces.cpp -o auto_braces
$ ./auto_braces
a = 5, b = 5, c holds 1 value, d holds 3 values
$ clang++ -std=c++23 -Wall -Wextra vector_forms.cpp -o vector_forms
$ ./vector_forms
a = [7, 7, 7]
b = [3, 7]
c = [0, 0, 0]
d = [3]
$ g++-16 -std=c++23 -Wall -Wextra auto_braces.cpp -o auto_braces
$ ./auto_braces
a = 5, b = 5, c holds 1 value, d holds 3 values
$ g++-16 -std=c++23 -Wall -Wextra vector_forms.cpp -o vector_forms
$ ./vector_forms
a = [7, 7, 7]
b = [3, 7]
c = [0, 0, 0]
d = [3]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 braces_session.py

The static_assert lines in auto_braces.cpp check the deduced types while the file compiles, so the build itself proves that b is an int and c a list. For containers, use parentheses for a count or a size, and braces for the elements themselves.

Which form to use

Most of this lesson reduces to a few habits, which match rules ES.20 and ES.23 of the C++ Core Guidelines:

  • Initialise every variable where you declare it. If there is no sensible value yet, declare the variable later, where the value is known.
  • Prefer braces for values, int seats{40};, so that the compiler rejects narrowing, and write int seats{}; for zero.
  • With auto, use the equals sign: auto seats = 40;.
  • For constructors that take a count or a size, such as std::vector<int> rows(3, 0);, use parentheses.
  • When you mean to lose information, write static_cast or std::lround, so the conversion is visible.

Try it: fix six declarations

Each of the six declarations in main below has one mistake. Fix them so that the file builds with -Wall -Wextra -Werror on both compilers and prints seats 0, per row 7, rows [0, 0, 0], hall size 0, discount 10, label Gate A.

fix_me.cpp C++ · fix_me.cpp
#include <print>
#include <string>
#include <vector>

// Each of the six declarations in main() has one mistake. Fix them so that the file
// builds with -Wall -Wextra -Werror on both compilers and prints:
// seats 0, per row 7, rows [0, 0, 0], hall size 0, discount 10, label Gate A
int main() {
    int seats;
    int per_row{7.5};
    std::vector<int> rows{3, 0};
    std::string hall();
    unsigned discount{-10};
    auto label = {"Gate A"};
    std::println("seats {}, per row {}, rows {}, hall size {}, discount {}, label {}",
                 seats, per_row, rows, hall.size(), discount, label);
}

The compilers report four of the mistakes; the other two compile and print the wrong thing:

What the compilers say about fix_me.cpp Python · fix_me_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -Werror fix_me.cpp -o fix_me
g++-16 -std=c++23 -Wall -Wextra -Werror fix_me.cpp -o fix_me
""", max_lines=60)

Output

$ clang++ -std=c++23 -Wall -Wextra -Werror fix_me.cpp -o fix_me
fix_me.cpp:10:17: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
   10 |     int per_row{7.5};
      |                 ^~~
fix_me.cpp:10:17: note: insert an explicit cast to silence this issue
   10 |     int per_row{7.5};
      |                 ^~~
      |                 static_cast<int>( )
fix_me.cpp:10:17: error: implicit conversion from 'double' to 'int' changes value from 7.5 to 7 [-Werror,-Wliteral-conversion]
   10 |     int per_row{7.5};
      |                ~^~~
fix_me.cpp:12:21: error: empty parentheses interpreted as a function declaration [-Werror,-Wvexing-parse]
   12 |     std::string hall();
      |                     ^~
fix_me.cpp:12:21: note: remove parentheses to declare a variable
   12 |     std::string hall();
      |                     ^~
fix_me.cpp:13:23: error: constant expression evaluates to -10 which cannot be narrowed to type 'unsigned int' [-Wc++11-narrowing]
   13 |     unsigned discount{-10};
      |                       ^~~
fix_me.cpp:13:23: note: insert an explicit cast to silence this issue
   13 |     unsigned discount{-10};
      |                       ^~~
      |                       static_cast<unsigned int>( )
fix_me.cpp:16:40: error: base of member reference is a function; perhaps you meant to call it with no arguments?
   16 |                  seats, per_row, rows, hall.size(), discount, label);
      |                                        ^~~~
      |                                            ()
5 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -Werror fix_me.cpp -o fix_me
fix_me.cpp: In function 'int main()':
fix_me.cpp:10:17: error: narrowing conversion of '7.5e+0' from 'double' to 'int' [-Wnarrowing]
   10 |     int per_row{7.5};
      |                 ^~~
fix_me.cpp:12:21: error: empty parentheses were disambiguated as a function declaration [-Werror=vexing-parse]
   12 |     std::string hall();
      |                     ^~
fix_me.cpp:12:21: note: remove parentheses to default-initialize a variable
   12 |     std::string hall();
      |                     ^~
      |                     --
fix_me.cpp:13:23: error: narrowing conversion of '-10' from 'int' to 'unsigned int' [-Wnarrowing]
   13 |     unsigned discount{-10};
      |                       ^~~
fix_me.cpp:16:45: error: request for member 'size' in 'hall', which is of non-class type 'std::string()' {aka 'std::__cxx11::basic_string<char>()'}
   16 |                  seats, per_row, rows, hall.size(), discount, label);
      |                                             ^~~~
cc1plus: all warnings being treated as errors
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fix_me_session.py

One way to fix it

Read the errors from the top: the narrowing 7.5, the empty parentheses after hall, and the narrowing -10; the error about hall.size() disappears once hall is a variable. Two mistakes compile: rows{3, 0} holds the two elements 3 and 0, and auto label = {"Gate A"} makes a list rather than a string. The sixth, int seats;, is the quietest of all:

fixed.cpp C++ · fixed.cpp
#include <print>
#include <string>
#include <vector>

int main() {
    int seats{};                  // value-initialised: 0
    int per_row{7};               // a whole number to begin with (or static_cast<int>(7.5))
    std::vector<int> rows(3, 0);  // parentheses: 3 elements, each 0
    std::string hall;             // a variable: no empty parentheses
    unsigned discount{10};        // a value that fits an unsigned type
    std::string label{"Gate A"};  // a string, not an initializer_list
    std::println("seats {}, per row {}, rows {}, hall size {}, discount {}, label {}",
                 seats, per_row, rows, hall.size(), discount, label);
}
The fixed file on both compilers Python · fixed_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -Werror fixed.cpp -o fixed
./fixed
g++-16 -std=c++23 -Wall -Wextra -Werror -O2 fixed.cpp -o fixed
./fixed
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -Werror fixed.cpp -o fixed
$ ./fixed
seats 0, per row 7, rows [0, 0, 0], hall size 0, discount 10, label Gate A
$ g++-16 -std=c++23 -Wall -Wextra -Werror -O2 fixed.cpp -o fixed
$ ./fixed
seats 0, per row 7, rows [0, 0, 0], hall size 0, discount 10, label Gate A

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fixed_session.py

Had seats kept its missing initialiser, the warnings would not have caught it. std::println takes its arguments by reference, so inside main the compilers see seats passed along, not read. Here is that one line on its own:

seats_bug.cpp C++ · seats_bug.cpp
#include <print>

int main() {
    int seats;                         // still no initialiser
    std::println("seats {}", seats);   // println takes its arguments by reference
}
Four builds, one warning Python · seats_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -c seats_bug.cpp
clang++ -std=c++23 -Wall -Wextra -O2 -c seats_bug.cpp
g++-16 -std=c++23 -Wall -Wextra -c seats_bug.cpp
g++-16 -std=c++23 -Wall -Wextra -O2 -c seats_bug.cpp
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -c seats_bug.cpp
$ clang++ -std=c++23 -Wall -Wextra -O2 -c seats_bug.cpp
$ g++-16 -std=c++23 -Wall -Wextra -c seats_bug.cpp
$ g++-16 -std=c++23 -Wall -Wextra -O2 -c seats_bug.cpp
In file included from seats_bug.cpp:1:
In function 'void std::println(format_string<_Args ...>, _Args&& ...) [with _Args = {int&}]',
    inlined from 'int main()' at seats_bug.cpp:5:17:
/opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/print:164:19: warning: 'seats' is used uninitialized [-Wuninitialized]
  164 |     { std::println(stdout, __fmt, std::forward<_Args>(__args)...); }
      |       ~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
seats_bug.cpp: In function 'int main()':
seats_bug.cpp:4:9: note: 'seats' was declared here
    4 |     int seats;                         // still no initialiser
      |         ^~~~~

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 seats_session.py

Only GCC with -O2 reports it, from inside the <print> header, after inlining std::println into main. Warnings are a safety net with holes; the initialiser is the fix.

Interview questions

Fresher · What is the difference between int x(2.9); and int x{2.9};?

Both try to initialise an int from a double, but only the first is valid. int x(2.9); is direct-initialisation, which allows the conversion and drops the fraction, so x is 2; a compiler may warn, but the program builds. int x{2.9}; is list-initialisation, and converting a floating-point value to an integer type is a narrowing conversion, which list-initialisation forbids, so the program is ill-formed and both Clang and GCC reject it. If the truncation is intended, int x{static_cast<int>(2.9)}; states it. A strong answer adds that GCC reports narrowing from a non-constant only as a warning unless -pedantic-errors or -Werror=narrowing is given.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What happens to this line?

    Read the code, then choose one answer.

    int x{2.9};
    Show the answer to question 1

    Answer: It does not compile, because converting a double to int is narrowing

    Braces forbid narrowing conversions, and a floating-point value to an integer type is narrowing even when the value is a constant. Both Clang and GCC reject the line, because the value is a constant; with a variable of type double in the braces, GCC would only warn. int x(2.9); or int x = 2.9; would compile and store 2.

  2. Question 2 of 6 Which of these declares a function rather than a variable?

    Choose one answer.

    Show the answer to question 2

    Answer: std::string s();

    Empty parentheses can be read as an empty parameter list, and C++ reads anything that can be a declaration as one: s is a function that takes nothing and returns a std::string. The other three each make an empty string.

  3. Question 3 of 6 How many elements does v hold?

    Read the code, then choose one answer.

    std::vector<int> v{4, 1};
    Show the answer to question 3

    Answer: 2

    Braces choose the std::initializer_list constructor whenever a class has one, so v holds the two elements 4 and 1. std::vector<int> v(4, 1); would hold four elements, each 1.

  4. Question 4 of 6 What is the type of n in auto n = {5};?

    Choose one answer.

    Show the answer to question 4

    Answer: std::initializer_list<int>

    With an equals sign and braces, auto deduces a std::initializer_list. Without the equals sign, auto n{5}; makes an int, a rule C++17 adopted from proposal N3922.

  5. Question 5 of 6 These variables are declared inside a function. Which of them start with a defined value?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • int a{};
    • static int b;
    • std::string d;

    Empty braces value-initialise a to 0, a static variable is zero-initialised before main starts, and std::string has a default constructor that makes an empty string. A local int or double with no initialiser has an indeterminate value, and reading it is undefined behaviour in C++23.

  6. Question 6 of 6 GCC builds int c{price};, where price is a double variable, with only a warning. Which option makes it an error?

    Choose one answer.

    Show the answer to question 6

    Answer: -pedantic-errors

    GCC treats narrowing from a value that is not a constant as a warning, so that older code keeps building. -pedantic-errors (or -Werror=narrowing) turns it into the error the standard asks for. -Wextra adds other warnings, and neither -O2 nor a newer standard changes this one.

Key takeaways

  • int a = 1;, int b(2); and int c{3}; all initialise; the forms with braces are called list-initialisation.
  • Braces reject narrowing conversions such as double to int, int to double and int to unsigned, unless the value is a constant that fits. GCC only warns for a non-constant; -pedantic-errors makes it an error.
  • A local int, double or pointer declared with no initialiser has an indeterminate value, and reading it is undefined behaviour in C++23. Apple clang warns about a direct read at once, GCC only when optimising, and neither sees a read that happens through a reference.
  • C++26 makes that read erroneous behaviour; GCC 16 implements it by zero-filling, Clang does not yet. Write int total{}; and stop worrying about either rule.
  • std::string name(); declares a function. Write std::string name; or std::string name{};.
  • auto x{5} is an int, auto y = {5} is a std::initializer_list<int>; with containers, (3, 7) and {3, 7} mean different things.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.