C++11 Feature Highlights: New Keywords
14 May 2019C++11 introduced a large number of new features. This article provides a brief overview of each feature without going into too much detail—covering every aspect thoroughly would require an entire blog post for each feature.
To organize these new features, this series is divided into three parts: new keywords, new semantics, and new additions to the standard library. Initially, I planned to include all three sections in a single blog post, but it turned out to be too long, so I decided to split them into separate articles.
0. Key Feature Overview
In my opinion, the four most significant features in C++11 are auto, uniform initialization ({}), rvalue references, and lambda expressions.
autosimplifies variable declarations.- Uniform initialization (using
{}) standardizes initialization syntax. - Rvalue references enable move semantics, significantly improving performance.
- Lambda expressions make code more concise and enhance readability, especially when defined within function bodies, improving encapsulation and aligning better with everyday programming thought processes.
Of course, other features, such as variadic templates and the multithreading library, are also noteworthy and deserve in-depth discussion. However, given my limited expertise, I can only provide a brief summary of various features, which I refer to as a “highlights collection.”
1. New Keywords
1.1 auto, decltype
In fact, the auto keyword was already used in C++98 to indicate that a variable had an automatic storage duration. However, it was rarely used in practice. The C++11 standard deprecated this old usage and repurposed auto for automatic type deduction.
The introduction of auto significantly reduces code length—allowing the compiler to infer the type of an identifier instead of requiring manual specification. In most cases, the compiler can clearly determine the exact type of an identifier.
vector<int> arr;
......
vector<int>::iterator it = arr.begin();
auto it2 = arr.begin();// This is way more convenient than the previous code.
However, the introduction of auto also has a minor drawback—it sacrifices some code readability. For example:
auto value = myFunction();
The compiler will clearly know the type of value, but the programmer may not—unless they check the function declaration of myFunction. However, overall, the benefits far outweigh the drawbacks.
When initializing a variable with a value or expression, we can use auto to declare the variable, allowing the compiler to automatically deduce its type. However, in some cases, we may not want to initialize a variable with a value or expression but instead declare a variable using only the type of a value. In such situations, we can use the decltype keyword, which can extract the type of a variable or expression:
// The type of `value` is the return type of `myFunction`. The function `myFunction` is not actually executed here.
decltype(myFunction()) value;
It is worth noting that the result deduced by decltype is closely related to the form of the expression. If the expression is enclosed in one or more pairs of parentheses, the resulting type will be a reference type:
int a = 10;
decltype(a) b; // Correct, the type of `b` is `int`, and `b` is uninitialized.
decltype((a)) c; // Error, the type of `c` is `int&`, and it must be initialized.
1.2 =default, =delete, override, final
These newly introduced keywords are used in class design. The =default and =delete keywords modify functions that the compiler automatically generates (such as constructors and assignment operator overloads).
=defaultexplicitly declares that the compiler should generate the function.=deleteexplicitly declares that the compiler should not generate the function.
For example:
class Demo{
public:
Demo(const int a):m_a{a}{}
private:
int m_a;
};
At this point, the compiler will no longer generate a default constructor for Demo. If you want to use the compiler-generated default constructor, you can do it like this:
class Demo{
public:
Demo() =default;
Demo(const int a):m_a{a}{}
private:
int m_a;
};
You can prevent the copy constructor of Demo2 like this:
class Demo2{
public:
Demo(const Demo& obj) =delete;
private:
int m_a;
};
override and final are used for inheritance control.
-
overridetells the compiler that a subclass is overriding a virtual function from the parent class. It ensures that the overridden function in the subclass has the same signature as the virtual function in the base class, preventing issues where a function is mistakenly declared with an incorrect parameter list, rendering the override ineffective. -
finalhas two purposes:- When applied to a class, it prevents the class from being inherited.
- When applied to a virtual function, it prevents the function from being overridden by subclasses.
class FinalBase final{}
class A:public FinalBase{} // Error: `FinalBase` cannot be inherited.
class B{
public:
virtual int funcA(int x){}
virtual int funcB(int x) final{}
}
class C:public B{
public:
virtual int funcA(int x) override{...} // Ok, overriding `funcA`.
virtual int funcB(int x) {...} // Error: `funcB` has been declared as `final`.
}
1.3 nullptr
With the introduction of nullptr, the use of NULL or 0 to represent a null pointer should be avoided. Both NULL and 0 are essentially integers rather than pointer types. nullptr enforces stricter type checking.
Consider the following code:
void f(void*){...}
void f(int){...}
int main(){
f(NULL); // This call is ambiguous.
f(nullptr);//OK,call void f(void*)
1.4 constexpr
This keyword is very similar to const, making it easy to confuse the two.
constcan represent both compile-time constants and runtime constants, with an emphasis on the “read-only” characteristic.constexpr, on the other hand, is exclusively used for compile-time constants, indicating that the expression’s value can be determined at compile time, allowing the compiler to optimize it as much as possible.
int func(int p){
const int a = 5; // `a` is a compile-time constant.
const int b = p; // `b` is a runtime constant.
constexpr int c = 6; // `c` is a compile-time constant.
constexpr int c = p; // Compilation error.
std::array<int, a> arr1; // Ok.
std::array<int, b> arr2; // Compilation error.
std::array<int, c> arr3; // Ok.
}
Additionally, if a function’s return value is marked with constexpr, the compiler will attempt to evaluate the function call result at compile time whenever possible:
constexpr int calcLength(const int& x){
return x * 5;
}
int calcLength2(const int& x){
return x * 5;
}
std::array<int, calcLength(2)> arr10; // ✅ OK, `arr10` is an array of 10 elements.
// `calcLength(2)` is evaluated at compile time, and `10` replaces `calcLength(2)` directly.
std::array<int, calcLength2(3)> arr15; // ❌ Compilation error: `calcLength2` is not a `constexpr` function.
int a{5};
int b = calcLength(a); // ✅ OK, but `calcLength(a)` is no longer evaluated at compile time
// because `a` is a variable (not a constant expression).
constexpr c = calcLength(a); // ❌ Compilation error: `calcLength(a)` cannot be a `constexpr`
// because `a` is a runtime variable, making the result non-constant.
Additionally, both const and constexpr can be used to modify pointers, but they have different meanings.
constcan serve as both low-level const (modifying the pointed-to content) and top-level const (modifying the pointer itself).constexpr, when applied to pointers, only modifies the pointer itself and does not affect the pointed-to content.
Example:
int x = 2;
const int *a; // `a` points to a constant `int`.
int* const b = &x; // `b` is a constant pointer to an `int` variable.
// Once initialized, `b` cannot change its target—it is a pointer constant.
constexpr int *c = nullptr; // `c` is a pointer constant.
// It must be initialized at definition, the value must be determinable at compile time, and its target cannot be changed afterward.
1.5 noexcept
The noexcept keyword tells the compiler that the function will not throw exceptions.
This allows the compiler to perform additional optimizations.