Welcome to follow and continuously share insights on learning C++
Selected Previous Articles
C++ std::thread: A Cross-Platform Thread Management Tool for Concurrency
From Explicit to Smart: Core Features of C++ Class Templates
C++ Exception Handling: From Crashes to the Art of Elegant Error Management

Since the birth of C++11 and the release of C++23 in 2023, the significant upgrades over the past decade have gradually pushed this ancient language towards modernization. Among these, the evolution of modifiers and specifiers has greatly changed the design and coding practices of programs. Today, we will systematically summarize some of the most commonly used modifiers and specifiers, and see how they make C++ code more efficient, safer, and more elegant.
constexpr: A Revolution in Compile-Time Computation
Before C++11, we commonly used const to define constants, but const only guarantees “not modifiable at runtime” and does not necessarily allow for compile-time evaluation. The core function of constexpr is to enforce that expressions are evaluated at compile time, which can modify both variables and functions, making it a core tool for achieving “compile-time programming”.
This feature allows variables and functions to be evaluated at compile time, moving computation from runtime to compile time, resulting in significant performance improvements.
// C++11 style constexpr function (only return)constexpr int add11(int a, int b){ return a + b;}// C++14 style constexpr function (supports multiple statements)constexpr int add14(int a, int b){ int res = a + b; res *= 2; return res;}int main(){ // Compile-time computation: result is 6 constexpr int res1 = add11(1, 2); // Compile-time computation: result is 10 constexpr int res2 = add14(2, 3); int x = 4; // Runtime computation: x is a runtime variable, at this point constexpr function degrades to a normal function int res3 = add14(x, 5); cout << res1 << " " << res2 << " " << res3 << endl; return 0;}
As the standard evolves, the capabilities of constexpr continue to enhance. C++14 allows constexpr functions to include local variables and loops, C++17 further relaxes restrictions on lambda expressions and conditional branches, and by C++20, even virtual functions and try-catch blocks are supported.
// C++20 constexpr virtual functionstruct Base{ virtual constexpr int value() const { return 0; }};struct Derived : Base{ constexpr int value() const override { return 42; }};constexpr Derived d;constexpr int v = d.value(); // Compile-time polymorphism, v=42
Usage Instructions:
-
constexpr variables must be initialized at the time of definition, and the initialization expression must be a constant expression
- For functions modified by constexpr, if the passed parameters are compile-time constants, the function is evaluated at compile time; if runtime variables are passed, the function degrades to a normal function and executes at runtime
-
Destructors of constexpr objects must be constexpr (C++20)
consteval and constinit: Enforcing Compile-Time Computation
C++20 introduced two new keywords: consteval and constinit, further refining the compile-time computation system. consteval can only modify functions and is stricter than constexpr, requiring functions to be evaluated at compile time; constinit can only modify variables, ensuring that variables are initialized at compile time but does not require them to be constants.
// consteval function: must be evaluated at compile timeconsteval int cube(int x){ return x * x * x;}constexpr int c = cube(3); // Correct, compile-time computationint x = 3;// int d = cube(x); // Error, x is not a constant expression// constinit variable: initialized at compile time, but not a constantconstinit int buffer_size = 1024; // Initialized at compile timevoid resize_buffer(){ buffer_size = 2048; // Modifiable at runtime}
Comparison of constexpr, consteval, and constinit:
-
constexpr: can be evaluated at compile time or runtime, variables are const
-
consteval: must be evaluated at compile time, can only be used for functions
-
constinit: ensures variables are initialized at compile time, and variables are not constants and can be modified
Usage Instructions:
-
consteval functions cannot be declared as constexpr
-
constinit can only modify variables with static storage duration (global, static local, class static members), modifying local non-static variables will result in a compilation error
-
constinit cannot be used with constexpr simultaneously
-
consteval functions can call other consteval functions but cannot call normal functions
decltype: The Black Magic of Type Deduction
The decltype keyword introduced in C++11 is the core tool for compile-time type deduction, with the primary function of accurately deducing the type of expressions or variables, commonly used in templates, generally in conjunction with auto. However, the difference from auto is that while auto only focuses on the type of the variable, decltype pays more attention to the type of the expression and retains all characteristics of the original type, including const, volatile, references, etc.
#include <vector>using namespace std;// Postfix return type: deduce return type as vector<T>::iteratortemplate <typename T>auto get_iter(T& vec) -> decltype(vec.begin()){ return vec.begin();}int main(){ int a = 10; const int b = 20; int& c = a; int&& d = 30; // Deduced as int (same type as a) decltype(a) x = 5; // Deduced as const int (retains const modifier) decltype(b) y = 15; // Deduced as int& (retains reference property) decltype(c) z = a; // Deduced as int&& (retains rvalue reference property) decltype(d) w = 40; // Deduced as int (literal 5's original type is int) decltype(5) m = 0; z = 20; // z is a reference to a, modifying z is equivalent to modifying a cout << a << endl; // Outputs 20 vector<int> vec = {1,2,3}; auto it = get_iter(vec); cout << *it << endl; // Outputs 1 return 0;}
Usage of decltype(auto):C++14 introduced decltype(auto), combining the simplicity of auto and the precision of decltype
int global = 100;// Deduced as int& (because (global) is an lvalue expression)decltype(auto) get_global(){ return (global);}int main(){ auto val1 = get_global(); // val1 is int (auto discards reference) decltype(auto) val2 = get_global(); // val2 is int& (retains reference) val2 = 200; cout << global << endl; // Outputs 200 return 0;}
Note that decltype((x)) will deduce a reference type, while decltype(x) deduces the variable type
int main(){ int a = 10; int b = 20; // a+b is an rvalue expression, deduced as int decltype(a + b) x = 30; // (a) is an lvalue expression (a variable in parentheses is still an lvalue), deduced as int& decltype((a)) y = a; y = 50; // Modifying y is equivalent to modifying a cout << a << endl; // Outputs 50 return 0;}
noexcept: The Guardian of Exception Safety
C++11 introduced the noexcept specifier, which tells the compiler “this function will not throw exceptions” (or “whether exceptions are thrown is determined by the expression”), allowing the compiler to optimize based on this promise and informing callers about the function’s exception behavior.
// Declare a function that does not throw exceptionsvoid swap(int& a, int& b) noexcept{ int temp = a; a = b; b = temp;}// Conditional noexcept: determines whether an expression may throw exceptions based on the expressiontemplate<typename T>void destroy(T* ptr) noexcept(std::is_nothrow_destructible_v<T>){ ptr->~T();}// Exception specifier as an overload resolution basisvoid process(int){ std::cout << "Processing int" << std::endl;}void process(double) noexcept{ std::cout << "Processing double(noexcept)" << std::endl;}int main(){ int a = 0; double b = 0.0; process(a); // Calls process(int) process(b); // Calls process(double) return 0;}
Effects of noexcept:
-
If a container’s move constructor is noexcept, it will use move instead of copy when reallocating memory, improving performance
-
The compiler can omit exception handling code, reducing binary size
-
Calls to noexcept functions can be optimized, such as inlining
-
After C++11, destructors are defaulted to noexcept; if a destructor throws an exception, it will cause std::terminate to be called, terminating the program, so do not throw exceptions in destructors
override and final: Safety Nets for Inheritance Hierarchies
C++11 introduced the override and final keywords, enhancing the safety and expressiveness of inheritance hierarchies. When overriding a base class virtual function, if the function signature (return type, parameters, const, etc.) is inconsistent, the compiler will not report an error but will consider it a new virtual function. This is generally a bug, and the purpose of override is to explicitly inform the compiler “this function overrides a base class virtual function”; if the signature is inconsistent, the compiler will report an error.Additionally, final has two core uses: it can modify a class to prevent it from being inherited, and it can modify a virtual function to prevent derived classes from overriding this virtual function. When designing “non-extendable” classes or functions, final can clarify intent and allow the compiler to optimize.
class Base{public: virtual void draw() const { std::cout << "Drawing Base" << std::endl; } virtual void update() { std::cout << "Updating Base" << std::endl; } void calc() const { std::cout << "Calculating Base" << std::endl; }};class Derived : public Base{public: // override ensures it actually overrides the base class virtual function void draw() const override { std::cout << "Drawing Derived" << std::endl; } // Error example: base class update is not const, declaring here as const will cause a compilation error // void update() const override {} // final prevents this function from being further overridden void update() final { std::cout << "Updating Derived" << std::endl; }};class FinalClass final : public Derived{public: void draw() const override { std::cout << "Drawing FinalClass" << std::endl; }};// Error: cannot inherit from final class// class AnotherDerived : public FinalClass {};
Usage Instructions:
-
override can only be used for virtual functions
-
final can be used for classes, virtual functions, and member functions
-
override does not change the const or reference qualifiers of the function
-
If the base class function is not virtual, the derived class cannot declare it with override
thread_local: Thread-Private Variables
In multithreaded programming, thread-local storage is crucial. C++11 introduced the thread_local keyword, creating independent variable instances for each thread, avoiding data races between threads. Compared to the __thread extension of GCC/Clang compilers, thread_local is a standard feature of C++11, offering better portability and can be used for all data types.It solves the competition problem of traditional global/static variables in a multithreaded environment—no locks are needed, as each thread operates on its own copy, making it inherently thread-safe.
#include <iostream>#include <thread>#include <chrono>// Thread-local variablethread_local int counter = 0;void increment_counter(const std::string& thread_name){ for (int i = 0; i < 3; ++i) { counter++; std::cout << thread_name << ": counter = " << counter << std::endl; }}int main(){ std::thread t1(increment_counter, "Thread 1"); // Delay to avoid overlapping prints std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::thread t2(increment_counter, "Thread 2"); t1.join(); t2.join(); // Main thread's counter is still 0 std::cout << "Main thread: counter = " << counter << std::endl; return 0;}
Output Result:

Storage Duration of thread_local:
- Initialized upon first use, not at thread creation
-
Destroyed when the thread ends
-
Each thread has an independent instance copy
Common Usage Pitfalls:
-
thread_local cannot be used for function parameters or catch clause parameters
- When modifying class members with thread_local, they must also be declared as static
-
thread_local variables are destroyed after the thread ends and cannot be accessed again
-
The destructors of class thread_local variables are automatically called when the thread exits, so ensure that constructors/destructors are thread-safe (avoid accessing shared resources in constructors)
explicit: Rejecting Sneaky Implicit Conversions
Before C++11, explicit could only modify single-parameter constructors, but C++11 made a critical extension—it can now modify multi-parameter constructors (in conjunction with initializer lists) and can also modify conversion operators. Its core function is to “prohibit the compiler from automatically performing implicit type conversions”, allowing only explicit conversions, fundamentally avoiding hidden bugs caused by implicit conversions.
class Point{public: // Multi-parameter constructor with explicit, prohibits {}-style implicit conversion explicit Point(int x, int y) : m_x(x), m_y(y) {} void print() const { cout << "x=" << m_x << ", y=" << m_y << endl; }private: int m_x, m_y;};void print_Point(Point p){ p.print();}int main(){ // Implicit conversion: {3,4} attempts to convert to Point, explicit prohibits it, compilation error // print_Point({3,4}); // Explicit construction, valid print_Point(Point(3,4)); // Correct return 0;}
It can also be used to prevent class conversion operators (operator T ()) from performing implicit conversions:
class MyString{public: MyString(const char* str) : m_str(str) {} // Conversion operator: MyString→bool, adding explicit prohibits implicit conversion explicit operator bool() const { return !m_str.empty(); }private: string m_str;};int main(){ MyString s1("hello"); MyString s2(""); // Exception scenario: implicit use is allowed in conditional statements (C++ standard special provision) if (s1) { cout << "s1 is not empty" << endl; // Outputs: s1 is not empty } // Implicit conversion assignment: adding explicit prohibits it, compilation error // bool b = s1; // Must explicitly convert bool b1 = static_cast<bool>(s1); // Correct (true) bool b2 = static_cast<bool>(s2); // Correct (false) return 0;}
Summary: The Shift in Modern C++ Programming Paradigms
Since C++11, the evolution of the above modifiers and specifiers reflects C++’s continuous move towards compile-time computation, type safety, and performance optimization. These features are not just syntactic sugar; they represent a shift in C++ programming paradigms:
-
Compile-Time Programming: constexpr, consteval, constinit move more computations from runtime to compile time, enhancing performance
-
Type Safety: override and final enhance the safety of inheritance hierarchies; explicit prevents implicit conversions, avoiding accidental errors
-
Exception Safety: noexcept clarifies function exception behavior, aiding compiler optimization
-
Multithreading Support: thread_local simplifies thread-private storage management
-
Enhanced Generics: decltype in conjunction with templates accurately deduces types, supporting complex template parameter types
Of course, C++ has many other modifiers and specifiers, which cannot all be listed due to space constraints. Through these common examples, we can see that modern C++ is continuously evolving towards being safer, more efficient, and easier to maintain. As the C++ standard continues to develop, these tools will become more powerful, providing developers with more possibilities.