C++ provides four named cast operators:
static_cast<Type>(expr)const_cast<Type>(expr)reinterpret_cast<Type>(expr)dynamic_cast<Type>(expr) In contrast, the C style explicit cast (Type)expr looks way too innocent.
"An ugly behavior should have an ugly looking."
const_castCast away low-level constness (DANGEROUS):
int ival = 42;
const int &cref = ival;
int &ref = cref; // Error: casting away low-level constness
int &ref2 = const_cast<int &>(cref); // OK
int *ptr = const_cast<int *>(&cref); // OK
However, modifying a const object through a non-const access path (possibly formed by const_cast) results in undefined behavior!
const int cival = 42;
int &ref = const_cast<int &>(cival); // compiles, but dangerous
++ref; // undefined behavior (may crash)
reinterpret_castOften used to perform conversion between different pointer types (DANGEROUS):
int ival = 42;
char *pc = reinterpret_cast<char *>(&ival);
We must never forget that the actual object addressed by pc is an int, not a character! Any use of pc that assumes it's an ordinary character pointer is likely to fail at run time, e.g.:
std::string str(pc); // undefined behavior
Wherever possible, do not use it!
static_castOther types of conversions (which often look "harmless"):
double average = static_cast<double>(sum) / n;
int pos = static_cast<int>(std::sqrt(n));
Some typical usage:
static_cast<std::string &&>(str) // converts to a xvalue
static_cast<Derived *>(base_ptr) // downcast without runtime checking
[Best practice] Minimize casting. (Effective C++ Item 27)
Type systems work as a guard against possible errors: Type mismatch often indicates a logical error.
[Best practice] When casting is necessary, prefer C++-style casts to old C-style casts.
C++ is very good at type computations:
std::vector v(10, 42);
std::vector<int> v(10, 42);, but the compiler can deduce that int from 42.int x = 42; double d = 3.14; std::string s = "hello";
std::cout << x << d << s;
x, d and s and select the correct printing functions.autoWhen declaring a variable with an initializer, we can use the keyword auto to let the compiler deduce the type.
auto x = 42; // `int`, because 42 is an `int`.
auto y = 3.14; // `double`, because 3.14 is a `double`.
auto z = x + y; // `double`, because the type of `x + y` is `double`.
auto m; // Error: cannot deduce the type. An initializer is needed.
auto can also be used to produce compound types:
auto &r = x; // `int &`, because `x` is an `int`.
const auto &rc = r; // `const int &`.
auto *p = &rc; // `const int *`, because `&rc` is `const int *`.
autoWhat about this?
auto str = "hello";
autoWhat about this?
auto str = "hello"; // `const char *`
"hello" is const char [6], not std::string. This is for compatibility with C.auto, the array-to-pointer conversion ("decay") is performed automatically.autoDeduction of return type is also allowed (since C++14):
auto sum(int x, int y) {
return x + y;
}
int.Since C++20, auto can also be used for function parameters! Such a function is actually a function template.
auto sum(auto x, auto y) {
return x + y;
}
autoauto lets us enjoy the benefits of the static type system.
Some types in C++ are very long:
std::vector<std::string>::const_iterator it = vs.begin();
Use auto to simplify it:
auto it = vs.begin();
autoauto lets us enjoy the benefits of the static type system.
Some types in C++ are not known to anyone but the compiler:
auto lam = [](int x, int y) { return x + y; } // A lambda expression.
Every lambda expression has its own type, whose name is only known by the compiler.
decltypedecltype(expr) will deduce the type of the expression expr without evaluating it.
auto fun(int a, int b) { // The return type is deduced to be `int`.
std::cout << "fun() is called.\n"
return a + b;
}
int x = 10, y = 15;
decltype(fun(x, y)) z; // Same as `int z;`.
// Unlike `auto`, no initializer is required here.
// The type is deduced from the return type of `fun`.
decltype(fun(x, y)) only deduces the return type of fun without actually calling it. Therefore, no output is produced.auto and decltypeThe detailed rules of auto and decltype (as well as their differences) are complicated, and require some deeper understanding of C++ types and templates. You don't have to remember them.
Learn about them mainly through experiments.
C23 also has auto type deduction.
Some functions have parameters that are given a particular value in most, but not all, calls. In such cases, we can declare that common value as a default argument.
std::string get_screen(std::size_t height = 24, std::size_t width = 80,
char background = ' ');
By default, the screen is ' '.
auto default_screen = get_screen();
To override the default arguments:
auto large_screen = get_screen(66); // 66x80, filled with ' '
auto larger_screen = get_screen(66, 256); // 66x256, filled with ' '
auto special_screen = get_screen(66, 256, '#'); // 66x256, filled with '#'
Arguments in the call are resolved by position.
auto scr = get_screen('#'); // Passing the ASCII value of '#' to `height`.
// `width` and `background` are set to
// default values (`80` and `' '`).
Some other languages have named parameters:
print(a, b, sep=", ", end="") # Python
There is no such syntax in C++.
Default arguments are only allowed for the last (right-most) several parameters:
std::string get_screen(std::size_t height = 24, std::size_t width,
char background); // Error.
In C++, a group of functions can have the same name, as long as they can be differentiated when called.
int max(int a, int b) {
return a < b ? b : a;
}
double max(double a, double b) {
return a < b ? b : a;
}
const char *max(const char *a, const char *b) {
return std::strcmp(a, b) < 0 ? b : a;
}
auto x = max(10, 20); // Calls max(int, int)
auto y = max(3.14, 2.5); // Calls max(double, double)
auto z = max("hello", "world"); // Calls max(const char *, const char *)
Overloaded functions should be distinguished in the way they are called.
int fun(int);
double fun(int); // Error: functions that differ only in
// their return type cannot be overloaded.
void move_cursor(Coord to);
void move_cursor(int r, int c); // OK, differ in the number of arguments
Overloaded functions should be distinguished in the way they are called.
The following are declaring the same function. They are not overloading.
void fun(int *);
void fun(int [10]);
The following are the same for an array argument:
void fun(int *a);
void fun(int (&a)[10]);
int ival = 42; fun(&ival); // OK, calls fun(int *)
int arr[10]; fun(arr); // Error: ambiguous call
Why?
Overloaded functions should be distinguished in the way they are called.
The following are the same for an array argument:
void fun(int *a);
void fun(int (&a)[10]);
int arr[10]; fun(arr); // Error: ambiguous call
fun(int (&)[10]), this is an exact match.fun(int *), this involves an array-to-pointer implicit conversion. We will see that this is also considered an exact match.Suppose we have the following overloaded functions.
void fun(int);
void fun(double);
void fun(int *);
void fun(const int *);
Which will be the best match for a call fun(a)?
Suppose we have the following overloaded functions.
void fun(int);
void fun(double);
void fun(int *);
void fun(const int *);
Obvious: The arguments and the parameters match perfectly.
fun(42); // fun(int)
fun(3.14); // fun(double)
const int arr[10];
fun(arr); // fun(const int *)
Not so obvious:
int ival = 42;
// fun(int *) or fun(const int *)?
fun(&ival);
fun('a'); // fun(int) or fun(double)?
fun(3.14f); // fun(int) or fun(double)?
fun(NULL); // fun(int) or fun(int *)?
void fun(int);
void fun(double);
void fun(int *);
void fun(const int *);
fun(&ival) matches fun(int *)fun('a') matches fun(int)fun(3.14f) matches fun(double)fun(NULL) ? We will see this later.There are detailed rules that define these behaviors. But our program should avoid such confusing overload sets.
const conversionconstNo need to remember all the details. But pay attention to some cases that are very common.
NULL is a macro defined in standard library header files.
(void *)0, 0, (long)0 or other forms.In C++, NULL cannot be (void *)0 since the implicit conversion from void * to other pointer types is not allowed.
It is most likely to be an integer literal with value zero.
With the following overload declarations, fun(NULL) may call fun(int) on some platforms, and may be ambiguous on other platforms!
void fun(int);
void fun(int *);
fun(NULL); // May call fun(int),
// or may be ambiguous.
nullptrIn short, NULL is a "fake" pointer.
Since C++11, a better null pointer is introduced: nullptr (also available in C23)
nullptr has a unique type std::nullptr_t (defined in <cstddef>), which is neither void * nor an integer.
fun(nullptr) will definitely match fun(int *).
void fun(int);
void fun(int *);
fun(NULL); // May call fun(int),
// or may be ambiguous.
fun(nullptr); // Calls fun(int *).
[Best practice] Use nullptr as the null pointer constant in C++.
Only overload operations that actually do similar things. A bad example:
Screen &moveHome(Screen &);
Screen &moveAbs(Screen &, int, int);
Screen &moveRel(Screen &, int, int, std::string direction);
If we overload this set of functions under the name move, some information is lost.
Screen &move(Screen &);
Screen &move(Screen &, int, int);
Screen &move(Screen &, int, int, std::string direction);
Which one is easier to understand?
moveHome(scrn); // OK, moves to home.
move(scrn); // Unclear: How to move?
for loops revisitedfor loopsTraverse a std::string
int str_to_int(const std::string &str) {
int value = 0;
for (auto c : str) // char
value = value * 10 + c - '0';
return value;
}
Note: This function can be replaced by std::stol.
for loopsTraverse a std::vector
bool is_all_digits(const std::string &str) {
for (auto c : str)
if (!std::isdigit(c))
return false;
return true;
}
int count_numbers(const std::vector<std::string> &strs) {
int cnt = 0;
for (const auto &s : strs) // const std::string &s
if (is_all_digits(s))
++cnt;
return cnt;
}
An array can also be traversed by range-for:
int arr[100] = {}; // OK in C++ and C23.
// The following loop will read 100 integers.
for (auto &x : arr) // int &
std::cin >> x;
for loop will traverse the entire array.What else can be traversed using a range-for?
void print(int *arr) {
for (auto x : arr) // Error: `arr` is a pointer, not an array.
std::cout << x << ' ';
std::cout << '\n';
}
We can declare arr to be a reference to array:
void print(const int (&arr)[100]) {
for (auto x : arr) // OK. `arr` is an array.
std::cout << x << ' ';
std::cout << '\n';
}
arr is of type const int (&)[100]: a reference to an array of 100 elements, where each element is of type const int.We can declare arr to be a reference to array:
void print(const int (&arr)[100]) {
for (auto x : arr) // OK. `arr` is an array.
std::cout << x << ' ';
std::cout << '\n';
}
arr is of type const int (&)[100]: a reference to an array of 100 elements, where each element is of type const int.Note that only arrays of 100 ints can fit here.
int a[100] = {}; print(a); // OK.
int b[101] = {}; print(b); // Error.
double c[100] = {}; print(c); // Error.
To allow arrays of any type, any length: Use a template function.
template <typename Type, std::size_t N>
void print(const Type (&arr)[N]) {
for (const auto &x : arr)
std::cout << x << ' ';
std::cout << '\n';
}
We will learn about this in the end of this semester.
Type system
constness, ...const_cast: used for casting away low-level constness.reinterpret_cast: used for conversion between different pointer types.static_cast: used for some normal "innocent-looking" conversions: int to double, unsigned to int, ...auto and decltype: type deductionFunctions
Range-based for loops