AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

02 Fundamental types / Lesson 5 of 6

const, constexpr and auto

14 min · +10 XP · 2 exercises · 7 questions

Read the text14 min
Exercises0/2
Quiz7 q

Quick answer

What is the difference between const and constexpr in C++?

In C++, const promises a variable will not change after initialisation, but its value may come from run time, even from input. constexpr promises the value is known while compiling, so it can be used as an array bound, a template argument or in static_assert. Every constexpr variable is also const; the reverse is not true.

Three keywords decide how much the compiler knows about a variable. const promises the value will not change after initialisation; constexpr promises it is known while compiling; auto asks the compiler to work the type out from the initialiser. Used well, each removes a class of mistakes — an accidental assignment, a run-time computation that should have been folded, a silently wrong numeric type. Used carelessly, each hides something. This lesson gives the rules, including the deduction rules for auto that most people learn by tripping over them.

const variables

const int limit = 100;
limit = 200;                      // error: assignment of read-only variable
const int rounds = readCount();   // fine: const is about after initialisation, not about compile time
const std::vector<int> table{1, 2, 3};
table.push_back(4);               // error: only const member functions may be called (Module 8)

A const object must be initialised and cannot be assigned afterwards. The initial value may come from anywhere, including input. The habit worth forming is const by default: declare every local const unless it changes, so that the ones that do change are the ones a reader looks at. It also turns a whole family of typos into compile errors, and it lets the compiler keep the value in a register without ever checking memory again.

const references

int x = 5;
const int& view = x;      // an alias that cannot be used to modify x
const double& d = 2.5;    // binds to a temporary and extends its lifetime to the reference's scope
int& bad = 2.5;           // error: a non-const lvalue reference cannot bind to a temporary

A const T& binds to anything — a variable, a literal, the result of a function call — without copying, which is why it is the default parameter type for anything bigger than a register (Module 4). A plain T& binds only to something that already has a name.

constexpr variables

constexpr int kMaxUsers = 1'000;
constexpr double kPi = 3.141592653589793;
int buckets[kMaxUsers / 10];               // a constant expression is required here
static_assert(kMaxUsers % 10 == 0);
constexpr int fromInput = readCount();     // error: not a constant expression

constexpr on a variable means "initialised by a constant expression and usable wherever the compiler needs a number": array bounds, template arguments, static_assert, case labels. Every constexpr variable is also const; the reverse is not true.

constexpr functions

constexpr long long factorial(int n) {
    long long result = 1;
    for (int i = 2; i <= n; ++i) result *= i;
    return result;
}

static_assert(factorial(20) == 2432902008176640000LL);   // evaluated by the compiler
int n;
std::cin >> n;
std::cout << factorial(n) << '\n';                        // evaluated at run time, same code

A constexpr function is an ordinary function that the compiler is allowed to evaluate during compilation when every argument is a constant and the result is needed in a constant expression. Otherwise it is called at run time like any other. C++20 permits loops, local variables, if, and even std::vector and std::string inside the compile-time evaluation (as long as they are gone by the end of it); it forbids undefined behaviour, so a constexpr call that overflows is a compile error rather than a silent wrap — a useful side effect. consteval (C++20) marks a function that must be evaluated at compile time, and constinit a variable that must be initialised then; Module 16 goes deeper.

constconstexpr
Value known atrun time is finecompile time, always
Can be initialised from inputyesno
Usable as an array bound or in static_assertonly if the initialiser was a constantyes
Applies to functions(member functions: Module 8)yes — "may run at compile time"

auto: the deduction rules

auto uses the same rules as template argument deduction (Module 12), which come down to four cases:

const int cx = 10;
int x = 5;

auto a = cx;          // int — a COPY; the reference-ness and top-level const of the initialiser are dropped
a = 11;               // fine, a is a mutable int
auto& r = cx;         // const int& — auto& keeps the reference and therefore the const
const auto& cr = x;   // const int& — binds to anything, never copies, never modifies
auto* p = &x;         // int* — say the star when you mean a pointer

Rule 1 is the one that matters: auto x = expr; is always a copy. That is safe for an int and expensive for a std::vector, and it is the reason the range-for idiom is for (const auto& item : items) when reading and for (auto& item : items) when modifying — for (auto item : items) item *= 2; doubles a copy and leaves the container untouched.

Literals fix the deduced type, so the literal's suffix is the type annotation:

auto i = 0;            // int
auto u = 0u;           // unsigned
auto l = 0L;           // long
auto ll = 0LL;         // long long
auto d = 0.0;          // double
auto f = 0.0f;         // float
auto c = 'c';          // char
auto s = "text";       // const char* — NOT std::string
auto big = 10'000'000'000;   // long: the literal did not fit an int
auto n = v.size();     // std::size_t — the right type without spelling it

auto list = {1, 2, 3}; deduces std::initializer_list<int>, which is almost never wanted; write std::vector<int>.

decltype in brief

decltype(expr) names the type of an expression without evaluating it. decltype(x) for a variable is its declared type; decltype((x)), with the extra parentheses, is the type of the expression x, an lvalue, so it is int&. It matters in templates (Module 12) and in decltype(auto) (Module 16); in everyday code you will meet it in decltype(v)::value_type and similar spellings.

When auto helps, and when it hides

auto earns its place when the type is obvious from the right-hand side (auto p = std::make_unique<Widget>()), when only the compiler can name it (a lambda, an iterator over a nested map), and when it prevents a silent conversion (auto n = v.size() is std::size_t; int n = v.size() narrows). It hides when the numeric type is the point:

auto total = 0;                 // int — overflows at 2 billion; write 0LL or long long total = 0;
auto mean = sum / count;        // integer division if both are integers; nobody can see it
auto name = "Ada";              // const char*; name.size() does not compile

The working rule: auto for types that are obvious or unnameable; the explicit type for numbers whose width or signedness matters, and for anything a reader would otherwise have to look up.

Pitfalls

  • auto x = someConstRef; makes a mutable copy — surprising when the copy is a large container.
  • auto& r = 5; does not compile; const auto& r = 5; does.
  • for (auto e : v) e = 0; modifies nothing.
  • auto sum = 0; then summing millions of values: it is an int.
  • constexpr int n = readInt(); — a run-time value can be const, never constexpr.

Key takeaways

  • const is a promise not to modify after initialisation; the value may come from run time. Make locals const by default.
  • const T& binds to anything, including temporaries, without a copy; T& needs an lvalue.
  • constexpr variables are compile-time constants; constexpr functions run at compile time when their arguments are constants and at run time otherwise, and static_assert proves it.
  • auto x = expr copies and drops references and top-level const; auto& and const auto& keep them; literal suffixes choose the numeric type.
  • Use auto when the type is obvious or unnameable; spell the type when a number's width or signedness matters.

Common questions

What does the auto keyword do in C++?

auto makes the compiler deduce a variable's type from its initialiser: auto n = v.size(); is std::size_t and auto d = 0.0; is double. It follows the rules of template argument deduction, so plain auto always makes a copy and drops references and top-level const.

What is a constexpr function in C++?

A constexpr function is an ordinary function the compiler may evaluate during compilation when every argument is a constant and the result is needed in a constant expression; otherwise it runs at run time like any other. A compile-time call that would overflow is a compile error. consteval (C++20) marks a function that must run at compile time.

What is the difference between auto, auto& and const auto&?

auto x = expr; makes a copy and drops references and top-level const. auto& binds a reference and keeps the const, so modifying through it changes the original. const auto& binds to anything, including temporaries, without copying — the idiom for reading elements in a range-based for loop.

Why does auto s = "text"; not give a std::string?

A string literal is an array of const char, and auto deduces the pointer it decays to, const char*, so s.size() does not compile. Write std::string s = "text";, or use the s suffix from std::string_literals: auto s = "text"s;.

When should you not use auto in C++?

Spell the type when a number's width or signedness matters. auto total = 0; is an int that overflows past about 2.1 billion, and auto mean = sum / count; hides an integer division. Use auto when the type is obvious from the right-hand side or cannot be named, such as a lambda or an iterator.

Mark as readSign in to keep your place.

C++ const vs constexpr and the auto Keyword Explained

In C++, const means a value cannot change after initialisation and constexpr means it is known at compile time. How auto deduces types, and when it hides a bug.

  • Course: C++ study plan
  • Module: Fundamental types
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: Clang 18 · C++20

What is the difference between const and constexpr in C++?

In C++, const promises a variable will not change after initialisation, but its value may come from run time, even from input. constexpr promises the value is known while compiling, so it can be used as an array bound, a template argument or in static_assert. Every constexpr variable is also const; the reverse is not true.

Lesson

Three keywords decide how much the compiler knows about a variable. const promises the value will not change after initialisation; constexpr promises it is known while compiling; auto asks the compiler to work the type out from the initialiser. Used well, each removes a class of mistakes — an accidental assignment, a run-time computation that should have been folded, a silently wrong numeric type. Used carelessly, each hides something. This lesson gives the rules, including the deduction rules for auto that most people learn by tripping over them.

const variables

const int limit = 100;
limit = 200;                      // error: assignment of read-only variable
const int rounds = readCount();   // fine: const is about after initialisation, not about compile time
const std::vector<int> table{1, 2, 3};
table.push_back(4);               // error: only const member functions may be called (Module 8)

A const object must be initialised and cannot be assigned afterwards. The initial value may come from anywhere, including input. The habit worth forming is const by default: declare every local const unless it changes, so that the ones that do change are the ones a reader looks at. It also turns a whole family of typos into compile errors, and it lets the compiler keep the value in a register without ever checking memory again.

const references

int x = 5;
const int& view = x;      // an alias that cannot be used to modify x
const double& d = 2.5;    // binds to a temporary and extends its lifetime to the reference's scope
int& bad = 2.5;           // error: a non-const lvalue reference cannot bind to a temporary

A const T& binds to anything — a variable, a literal, the result of a function call — without copying, which is why it is the default parameter type for anything bigger than a register (Module 4). A plain T& binds only to something that already has a name.

constexpr variables

constexpr int kMaxUsers = 1'000;
constexpr double kPi = 3.141592653589793;
int buckets[kMaxUsers / 10];               // a constant expression is required here
static_assert(kMaxUsers % 10 == 0);
constexpr int fromInput = readCount();     // error: not a constant expression

constexpr on a variable means "initialised by a constant expression and usable wherever the compiler needs a number": array bounds, template arguments, static_assert, case labels. Every constexpr variable is also const; the reverse is not true.

constexpr functions

constexpr long long factorial(int n) {
    long long result = 1;
    for (int i = 2; i <= n; ++i) result *= i;
    return result;
}

static_assert(factorial(20) == 2432902008176640000LL);   // evaluated by the compiler
int n;
std::cin >> n;
std::cout << factorial(n) << '\n';                        // evaluated at run time, same code

A constexpr function is an ordinary function that the compiler is allowed to evaluate during compilation when every argument is a constant and the result is needed in a constant expression. Otherwise it is called at run time like any other. C++20 permits loops, local variables, if, and even std::vector and std::string inside the compile-time evaluation (as long as they are gone by the end of it); it forbids undefined behaviour, so a constexpr call that overflows is a compile error rather than a silent wrap — a useful side effect. consteval (C++20) marks a function that must be evaluated at compile time, and constinit a variable that must be initialised then; Module 16 goes deeper.

constconstexpr
Value known atrun time is finecompile time, always
Can be initialised from inputyesno
Usable as an array bound or in static_assertonly if the initialiser was a constantyes
Applies to functions(member functions: Module 8)yes — "may run at compile time"

auto: the deduction rules

auto uses the same rules as template argument deduction (Module 12), which come down to four cases:

const int cx = 10;
int x = 5;

auto a = cx;          // int — a COPY; the reference-ness and top-level const of the initialiser are dropped
a = 11;               // fine, a is a mutable int
auto& r = cx;         // const int& — auto& keeps the reference and therefore the const
const auto& cr = x;   // const int& — binds to anything, never copies, never modifies
auto* p = &x;         // int* — say the star when you mean a pointer

Rule 1 is the one that matters: auto x = expr; is always a copy. That is safe for an int and expensive for a std::vector, and it is the reason the range-for idiom is for (const auto& item : items) when reading and for (auto& item : items) when modifying — for (auto item : items) item *= 2; doubles a copy and leaves the container untouched.

Literals fix the deduced type, so the literal's suffix is the type annotation:

auto i = 0;            // int
auto u = 0u;           // unsigned
auto l = 0L;           // long
auto ll = 0LL;         // long long
auto d = 0.0;          // double
auto f = 0.0f;         // float
auto c = 'c';          // char
auto s = "text";       // const char* — NOT std::string
auto big = 10'000'000'000;   // long: the literal did not fit an int
auto n = v.size();     // std::size_t — the right type without spelling it

auto list = {1, 2, 3}; deduces std::initializer_list<int>, which is almost never wanted; write std::vector<int>.

decltype in brief

decltype(expr) names the type of an expression without evaluating it. decltype(x) for a variable is its declared type; decltype((x)), with the extra parentheses, is the type of the expression x, an lvalue, so it is int&. It matters in templates (Module 12) and in decltype(auto) (Module 16); in everyday code you will meet it in decltype(v)::value_type and similar spellings.

When auto helps, and when it hides

auto earns its place when the type is obvious from the right-hand side (auto p = std::make_unique<Widget>()), when only the compiler can name it (a lambda, an iterator over a nested map), and when it prevents a silent conversion (auto n = v.size() is std::size_t; int n = v.size() narrows). It hides when the numeric type is the point:

auto total = 0;                 // int — overflows at 2 billion; write 0LL or long long total = 0;
auto mean = sum / count;        // integer division if both are integers; nobody can see it
auto name = "Ada";              // const char*; name.size() does not compile

The working rule: auto for types that are obvious or unnameable; the explicit type for numbers whose width or signedness matters, and for anything a reader would otherwise have to look up.

Pitfalls

  • auto x = someConstRef; makes a mutable copy — surprising when the copy is a large container.
  • auto& r = 5; does not compile; const auto& r = 5; does.
  • for (auto e : v) e = 0; modifies nothing.
  • auto sum = 0; then summing millions of values: it is an int.
  • constexpr int n = readInt(); — a run-time value can be const, never constexpr.

Key takeaways

  • const is a promise not to modify after initialisation; the value may come from run time. Make locals const by default.
  • const T& binds to anything, including temporaries, without a copy; T& needs an lvalue.
  • constexpr variables are compile-time constants; constexpr functions run at compile time when their arguments are constants and at run time otherwise, and static_assert proves it.
  • auto x = expr copies and drops references and top-level const; auto& and const auto& keep them; literal suffixes choose the numeric type.
  • Use auto when the type is obvious or unnameable; spell the type when a number's width or signedness matters.

Common questions

What does the auto keyword do in C++?

auto makes the compiler deduce a variable's type from its initialiser: auto n = v.size(); is std::size_t and auto d = 0.0; is double. It follows the rules of template argument deduction, so plain auto always makes a copy and drops references and top-level const.

What is a constexpr function in C++?

A constexpr function is an ordinary function the compiler may evaluate during compilation when every argument is a constant and the result is needed in a constant expression; otherwise it runs at run time like any other. A compile-time call that would overflow is a compile error. consteval (C++20) marks a function that must run at compile time.

What is the difference between auto, auto& and const auto&?

auto x = expr; makes a copy and drops references and top-level const. auto& binds a reference and keeps the const, so modifying through it changes the original. const auto& binds to anything, including temporaries, without copying — the idiom for reading elements in a range-based for loop.

Why does auto s = "text"; not give a std::string?

A string literal is an array of const char, and auto deduces the pointer it decays to, const char*, so s.size() does not compile. Write std::string s = "text";, or use the s suffix from std::string_literals: auto s = "text"s;.

When should you not use auto in C++?

Spell the type when a number's width or signedness matters. auto total = 0; is an int that overflows past about 2.1 billion, and auto mean = sum / count; hides an integer division. Use auto when the type is obvious from the right-hand side or cannot be named, such as a lambda or an iterator.

Exercises

constexpr power and primes

Implement two constexpr functions: long long power(long long base, int exponent) (exponent >= 0; the result fits in a long long) and bool isPrime(long long n) (trial division while d * d <= n; n >= 0). Then uncomment the two static_assert lines in the starter — they only compile if the functions really are usable at compile time — and answer queries: read q, then q lines that are either pow <base> <exponent> (print the value) or prime <n> (print yes or no).

Input: q, then q queries. Output: q lines.

Example: input 3 / pow 2 10 / prime 97 / prime 1 →

1024
yes
no

Deduced accumulators

Read an integer n (at least 1), then n integers in int range. Print sum=<exact sum> (it may exceed an int), int_mean=<sum / n in integer division>, mean=<sum / n with two decimals> and max=<largest value>.

Declare the accumulators with auto and let the initialiser fix the type: auto sum = 0LL; (a long long — auto sum = 0; would be an int and overflow) and auto best = std::numeric_limits<int>::min();. Make the derived values const auto, and cast to double for the real mean — sum / n with two integers is integer division no matter what auto deduces.

Input: n, then n integers. Output: four lines.

Example: input 4 1 2 3 4 →

sum=10
int_mean=2
mean=2.50
max=4

In this module: Fundamental types

← Conversions and casts · Literals and enumerations →