Programming skills and concepts in C/C++

C/C++中的编程技巧及其概念

Some examples in C++ that can be confusing or have peculiar usages are recorded.

Explicitly Qualified Number of Elements for Array Arguments

When an array is passed as a function parameter, it decays to a pointer:

A declaration of a parameter as “array of type” shall be adjusted to “qualified pointer to type”.

And as mentioned earlier:

int x[3][5];Here x is a 3 × 5 array of integers. When x appears in an expression, it is converted to a pointer to (the first of three) five-membered arrays of integers.

This means that when passing an array as a parameter, the bounds will be lost (C/C++ native arrays don’t have boundary checks either…).

1
2
3
void funcA(int x[10]){}
// Equivalent to
void funcB(int *x){}

The corresponding intermediate code is:

1
2
3
4
5
6
7
8
9
10
11
12
13
; Function Attrs: nounwind uwtable
define void @_Z5funcAPi(i32*) #4 {
%2 = alloca i32*, align 8
store i32* %0, i32** %2, align 8
ret void
}

; Function Attrs: nounwind uwtable
define void @_Z5funcBPi(i32*) #4 {
%2 = alloca i32*, align 8
store i32* %0, i32** %2, align 8
ret void
}

If the precise value of the array bounds is very important, and you want the function to only accept arrays containing a specific number of elements, you can use reference parameters:

1
void funcC(int (&x)[10]){}

The intermediate code is:

1
2
3
4
5
6
; Function Attrs: nounwind uwtable
define void @_Z5funcCRA10_i([10 x i32]* dereferenceable(40)) #4 {
%2 = alloca [10 x i32]*, align 8
store [10 x i32]* %0, [10 x i32]** %2, align 8
ret void
}

If we use an array with a number of elements not equal to 10 to pass to funcC, it will result in a compilation error:

1
2
3
4
5
6
7
8
9
// note: candidate function not viable: no known conversion from 'int [11]' to 'int (&)[10]' for 1st argument.
void funcC(int (&x)[10]){}
int main(int argc,char* argv[])
{
int x[11]={0,1,2,3,4,5,6,7,8,9,10};
// error: no matching function for call to 'funcC'.
funcC(x);
return 0;
}

You can also use function template parameters to specify the size of the array parameters the function accepts:

1
2
template<int arrSize>
void funcA(int x[arrSize]){}

Usage:

1
2
3
int x[12];
funcA<12>(x); // OK
funcA<13>(x); //ERROR

Enabling Compiler Warnings for Implicit Type Conversions Changing Sign

1
2
3
4
5
if((unsigned int)4<(unsigned int)(int)-1){
cout<<"yes"<<endl;
}else{
cout<<"no"<<endl;
}

The expression in the if statement is true (outputs yes), and no warning will be issued at compile time.

Even though we specified (int)-1, there will be an implicit conversion when comparing unsigned int and int. That is:

The usual arithmetic conversions are performed on operands of arithmetic or enumeration type.

1
((unsigned int)4<(unsigned)(int)-1)==true

Warnings about conversions between signed and unsigned integers are disabled by default in C++ unless -Wsign-conversion is explicitly enabled.

By enabling -Wsign-conversion, the warning can be seen (it is recommended to enable it).
The effect of this option is:

Warn for implicit conversions that may change the sign of an integer value, like assigning a signed integer expression to an unsigned integer variable. An explicit cast silences the warning. In C, this option is enabled also by -Wconversion.

For more GCC warning options, you can check here Warning-Options

Assert

assert Defined in header (c++)/<assert.h>(C)

If NDEBUG is defined as a macro name at the point in the source code where <assert.h> is included, then assert does nothing.
If NDEBUG is not defined, then assert checks if its argument (which must have scalar type) compares equal to zero.

1
2
3
4
5
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif

cppreference - assert
assert is only effective in debug mode; using it in release mode does nothing.
Because in VC++, release will globally define NDEBUG

The following code will compile and input a number >100 in VS with different results in debug and release mode (release will not).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

using namespace std;

bool func(int x) {
if (x > 100) {
return true;
}
else {
return false;
}
}
int main(void) {
int i;
cin >> i;
assert(func(i));
}

Invalid References

Typically, the references we create are valid, but they can also be rendered invalid due to human factors…

1
2
3
4
5
6
char* ident(char *p) { return p; }
int main(int argc,char* argv[])
{
char& r {*ident(nullptr)};
return 0;
}

This leads to undefined behavior.

In particular, a null reference cannot exist in a well-defined program, because the only way to create such a reference would be to bind it to the “object” obtained by indirection through a null pointer, which causes undefined behavior.

References to Arrays

1
2
3
4
5
6
7
8
9
10
void f(int(&r)[4]){
cout<<sizeof(r)<<endl;
}

void g(void){
int a[]={1,2,3,4};
f(a); // OK
int b[]={1,2,3};
f(b); // Error, number of elements is incorrect
}

For reference types of array parameters, the number of elements is also part of its type. Typically, references to arrays are only used in templates, where references can be inferred from the array.

1
2
3
4
5
6
7
8
9
10
11
template<class T,int N>
void f(T(&r)[N]){
// ...
}
int a1[10];
double a2[100];

void g(){
f(a1); // T is int, N is 10
f(a2); // T is double, N is 100
}

The consequence is that the more different types of arrays used to call f(), the more corresponding functions are defined.

Ignoring Top-Level cv-qualifier of Function Parameters

For compatibility with C, C++ automatically ignores the top-level cv-qualifier of the parameter type.

For example, the following function would report a redefinition error in C++, rather than an overload:

1
2
3
4
5
6
// Type is int(int)
int f(int x){}

// error: redefinition of 'f'
// Type is int(int)
int f(const int x){}

In either case, whether allowing or not allowing modification of the actual parameter, it is merely a copy of the actual argument provided by the function caller. Therefore, the calling process will not violate the data safety of the calling context.
The signature rule for functions is as follows:

<function> name, parameter type list (8.3.5), and enclosing namespace (if any)

And the function’s parameter-type-list will remove the top-level cv-qualifier:

[ISO/IEC 14882:2014 §8.3.5.5]After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type. The resulting list of transformed parameter types and the presence or absence of the ellipsis or a function parameter pack is the function’s parameter-type-list.

Be Careful with signed/unsigned when using char as an array index

When the char type is used as an array index, it should first be converted to unsigned char (since char is usually signed (implementation-defined)). It cannot be directly converted to int or unsigned int, or it will lead to array index out of bounds.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(void) {
char ch=-1;
printf("%d %u %d", (int)ch, (unsigned)ch, (unsigned char)ch);
return 0;
}
// output
// -1 4294967295 255

struct tag (*[5])(float)

The type designated as ‘struct tag (*[5])(float)’ has type ‘array of pointer to function returning struct tag’. The array has length five and the function has a single parameter of type float. Its type category is array.

new a pointer array

1
2
3
int TEN=10;
auto A=new (void(*[TEN])(void));
delete[] A;

Low-Level const and Top-Level const

Low-Level const: Indicates that the object pointed to by the pointer is a constant.
Top-Level const: Indicates that the pointer itself is constant. Top-Level const can indicate that any object is constant, which applies to any data type.

1
2
3
4
5
6
7
int ival=0;
int *const ivalp_1=&ival; // cannot change ivalp_1's value, this is a top-level const
const int ci=42; // cannot change ci's value, this is a top-level const
const int *ivalp_2=&ci; // allows changing ivalp_2's value, this is a low-level const

const int *const ivalp_3=ivalp_2; // right is top-level const, left is low-level const
const int &ref=ci; // the const used for declaring references are all low-level const

Actually, I have a simple method to distinguish: look at what is modified by const on the right.

  • For int const *x=std::nullput;, const modifies *x, as x is a pointer, we temporarily regard *x as dereference, which represents the object pointed to by x, so it is low-level const.
  • Conversely, int * const x=std::nullptr;, as const modifies the pointer x, so it is top-level const.

Dangers of Passing this Pointer in Constructor

If we pass the this pointer to other functions within a constructor, we could run into such problems:

1
2
3
4
5
6
struct C;
void no_opt(C*);
struct C {
int c;
C() : c(0) { no_opt(this); }
};

The code above seems fine, but when we construct a const C, it could lead to the following issue:

1
2
3
4
5
6
7
const C cobj;
void no_opt(C* cptr) {
int i = cobj.c * 100; // value of cobj.c is unspecified
cout<<i<<endl;
cout << cobj.c * 100 // value of cobj.c is unspecified
<< '\n';
}

The above code will compile successfully and can modify the constant object’s member i in no_opt.
Passing the this pointer of a constant object to other functions during construction means we could modify the values of the objects in that constant, which is not standard-compliant.

During the construction of a const object, if the value of the object or any of its subobjects is accessed through a glvalue that is not obtained, directly or indirectly, from the constructor’s this pointer, the value of the object or subobject thus obtained is unspecified.

Therefore, it’s best not to write anything that passes the this pointer outside the class in the constructor (it’s better to just initialize data members)…

Get the Absolute Path of the Current Executing Program

There are two methods:

1
2
3
4
#include <direct.h>
char buffer[MAXPATH];
getcwd(buffer, MAXPATH);
cout<<buffer<<endl;

This method has a drawback: if the executable program is added to the system PATH, it will obtain the path of the directory where it is executed.

Another method is through the Windows API:

1
2
3
4
5
6
const string getTheProgramAbsPath(void){
TCHAR exeFullPath[MAX_PATH]; // MAX_PATH is defined in WINDEF.h, equal to 260
memset(exeFullPath,0,MAX_PATH);
GetModuleFileName(NULL,exeFullPath,MAX_PATH);
return {exeFullPath};
}

In this way, irrespective of whether the program has been added to the system’s PATH or where it is executed, it will get the absolute path where this executable program is stored in the system.

A Quirky Usage of using

1
2
3
4
5
6
using foofunc=void(int);
foofunc foo;

int main(){
foo(1);
}

In the code above:

1
foofunc foo;

is declaring a function foo; looking at the symbol information in the target file (omitting unrelated details):

1
2
3
4
5
6
$ clang++ -c testusing.cc -o testusing.o -std=c++11
$ llvm-nm testusing.o
-------- U _Z3fooi
-------- U __main
-------- U atexit
00000050 T main

Through the c++filt toolchain in gcc, the symbols in the object file can be restored:

1
2
$ c++filt _Z3fooi
foo(int)

However, it is not defined, and directly linking will produce an undefined error.

Rvalue References

1
2
int x=123;
int &&y=x+1;

The IR code is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Using value 123 to initialize x
%2 = alloca i32, align 4
store i32 123, i32* %2, align 4
# y
%3 = alloca i32*, align 8
# Storing the temporary object created by x+1
%4 = alloca i32, align 4

# Calculate x+1
%5 = load i32, i32* %2, align 4
%6 = add nsw i32 %5, 1
# x+1 creates a temporary value that is %4
store i32 %6, i32* %4, align 4

# Bind the address of the temporary value to %3 (y)
store i32* %4, i32** %3, align 8

This realizes a non-copy behavior, which behaves similarly to assigning an object’s address to a pointer.
In fact, the purpose of rvalue references is to give temporary objects a longer lifespan—binding a reference to a temporary object without incurring any additional copy operations.
The same effect can be achieved with const T&:

1
2
int x=123;
const int &y=x+1;

And the above example will generate identical IR code under LLVM.

An Array Name Example

1
2
3
4
int a[]={1,2,3,4,5};
int *p=(int*)(&a+1);
printf("%d,%d\n",*(a+1),*(p-1));
// output: 2,5

How Many Passing Methods Are There?

Most people think there are the following three passing methods in C++ functions:

  • Pass by value: the value of the parameter is a copy of the actual parameter;
  • Pass by reference: the parameter is an alias for the actual parameter;
  • Pass by pointer: passing a pointer to the object to the parameter;

In fact, in C++, there are only two passing methods: pass by value, pass by reference.
Because passing by pointer is also a type of pass by value, the parameter value is merely a copy of the actual argument; they are just both pointers.
In the father of C++’s book: The C++ Programming Language 4th Edition, it states:

Unless a formal argument (parameter) is a reference, a copy of the actual argument is passed to the function.

Passing by pointer is merely a technique that utilizes the properties of pointers to avoid the overhead of copying, rather than a passing method.

The Three Laws of Defining Copy/Assign and Destructor Functions

If a class requires a user-defined copy constructor, copy assignment operator, or destructor, it typically needs all three.

The compiler-generated implicit definitions of copy constructor and operator= have a memberwise copy semantic, so if the operations generated by the compiler cannot meet the class’s copying needs (for instance, if class members are handles managing some resources), using the compiler’s implicit definitions will lead to shallow copies, causing two objects to enter some shared state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct A{
A():memory(nullptr){}
void getMemory(std::size_t memSize){
memory=(char*)malloc(memSize);
}
~A(){ free(memory); }
private:
char* memory;
};

int main()
{
A x;
x.getMemory(12);
A y;
y=x;
}

If the compiler-generated semantics are used, it would make objects x and y internally share a block of memory, so the user needs to define the copy constructor and copy assignment operator. For the same reason, when class members hold some resources, a user-defined destructor is also necessary.

Implementation of References

The C++ standard explains references like this:

[ISO/IEC 14882:2014 §8.3.2] A reference can be thought of as a name of an object.

However, the standard doesn’t require how this reference behavior should be implemented (which is often seen in the standard), but most compilers implement it using pointers in practice.
Consider the following code:

1
2
3
int a=123;
int &ra=a;
int *pc=&a;

Then compiling it to LLVM-IR to see the actual behavior of the compiler:

1
2
3
4
5
6
7
%2 = alloca i32, align 4
%3 = alloca i32*, align 8
%4 = alloca i32*, align 8

store i32 123, i32* %2, align 4
store i32* %2, i32** %3, align 8
store i32* %2, i32** %4, align 8

It’s evident that pointers and references have the exact same behavior post-compilation.

Appropriately Using Compiler-Generated Operations

In Implicit Declarations and Functions of Special Member Functions, it was mentioned that the compiler implicitly generates and defines the behavior of six kinds of special member functions.
Since the generated copy constructor and copy assignment operator both have a memberwise behavior, when the class we write can satisfy shallow copying (value semantics), there’s no need to write relevant operations at the expense, because the compiler-generated ones are just as good as your hand-written versions, and they are less error-prone.

1
2
3
4
5
6
7
struct A{
A(int a=0,double b=0.0):x(a),y(b){}
A(const A&)=default;
A& operator=(const A&)=default;
int x;
double y;
};

Although when you haven’t explicitly defined a copy constructor and copy assignment operator, the compiler will implicitly define them, it’s still better to manually use =delete to specify.
The compiler-generated version is exactly the same as the hand-written:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct A{
A(int a=0,double b=0.0):x(a),y(b){}
A(const A& r){
x=r.x;
y=r.y;
}
A& operator=(const A& r){
x=r.x;
y=r.y;
return *this;
}
int x;
double y;
};

It’s obvious that hand-writing is prone to error; this behavior can be safely handed over to the compiler.

Compressing Capacity and Truly Erasing Elements in STL Containers

Taken from C++ Programming Standards: 101 Rules/Guidelines and Best Practices Rule 82.

Compressing Container Capacity: The swap trick

1
2
3
4
5
vector<int> x{1,2,3,4,5,6,7};
// ...

vector<int>(x).swap(x); // Compress to appropriate capacity
vector<int>().swap(x); // Erase all elements

Truly Deleting Elements: std::remove does not perform the delete operation
The std::remove algorithm in STL does not truly remove elements from containers. As std::remove belongs to algorithm, it only operates over iterator ranges, not invoking container’s member functions, hence it cannot actually delete elements from containers.
Let’s take a look at the implementation in SGISTL (the implementation is a bit old and does not utilize std::move):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <class _InputIter, class _Tp>
inline _InputIter find(_InputIter __first, _InputIter __last, const _Tp& __val)
{
while (__first != __last && !(*__first == __val))
++__first;
return __first;
}

template <class _InputIter, class _OutputIter, class _Tp>
_OutputIter remove_copy(_InputIter __first, _InputIter __last, _OutputIter __result, const _Tp& __value) {
for ( ; __first != __last; ++__first)
if (!(*__first == __value)) {
*__result = *__first;
++__result;
}
return __result;
}

template <class _ForwardIter, class _Tp>
_ForwardIter remove(_ForwardIter __first, _ForwardIter __last, const _Tp& __value) {
__first = find(__first, __last, __value);
_ForwardIter __i = __first;
return __first == __last ? __first : remove_copy(++__i, __last, __first, __value);
}

You can see they are merely moving elements’ positions, not actually deleting the elements; they simply move the elements that should not be deleted to the front of the container, then return the new ending position iterator.
Effectively, the deleted portion is moved to the back of the element, so to truly delete all matching elements from the container, the erase-remove idiom needs to be used:

1
c.erase(std::remove(c.begin(),c.end(),value),c.end()); // Deletes elements at the tail of the container after std::remove

Beware of Hiding Overloaded Functions in Base Classes

If there is a virtual function func in the base class but it also overloads several non-virtual functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct A{
virtual void func(){
cout<<"A::func()"<<endl;
}
void func(int){
cout<<"A::func(int)"<<endl;
}
void func(double){
cout<<"A::func(double)"<<endl;
}
};

struct B:public A{
virtual void func(){
cout<<"B::func()"<<endl;
}
};

If we want to use the non-virtual version of the func function inside a B object:

1
2
3
B x;
// error: too many arguments to function call, expected 0, have 1
x.func(123);

This is because when the derived class overrides the base class virtual function, it hides the other overloaded functions, which needs to be explicitly brought into scope in B:

1
2
3
4
5
6
7
struct B:public A{
virtual void func(){
cout<<"B::func()"<<endl;
}
// Bring in the overloaded functions from A::func
using A::func;
};

Macro Alternatives

Macros are replaced in the preprocessing stage, at which point C++’s syntax and semantic rules are not yet effective; thus, macros can only perform simple text replacement, making them extremely blunt tools.
Macros are almost never required in C++. You can define understandable constants using const and enum. Use inline to avoid the overhead of function calls, templates to specify series of functions and types, and namespaces to avoid name conflicts.
Unless used for conditional compilation, there is no legitimate reason to use macros in C++.

Class Memory Allocation Functions

In C++, memory allocation functions for classes are all static member functions:

Any allocation function for a class T is a static member (even if not explicitly declared static).

This means operator new/operator delete and operator new[]/operator delete[] are implicitly declared as static member functions.

Exception Safety

  1. Destructors, operator new, operator delete must not throw exceptions.
  2. Swap operations should not throw exceptions.
  3. First do anything that may throw exceptions (but will not change important states of the object), and then end with operations that will not throw exceptions.
  4. When an exception is thrown from a throw expression towards a catch clause, any function executing in the path must manage to clean up any resources they control before removing their activation record from the execution stack.
  5. Do not insert code that might cause an early return, calls to functions that might throw exceptions, or other actions that will prevent the resource release at the end of the function.

cv Versions of Pointers to Class Member Functions

If we have a class A with overloaded member functions func that differ only by whether the member function is const, how do we define a pointer to the member function separately?

1
2
3
4
5
6
7
8
struct A{
void func()const{
std::cout<<"void func()const"<<std::endl;
}
void func(){
std::cout<<"void func()"<<std::endl;
}
};

If we just create a pointer to A::func, it points to the non-const version.

1
void(A::*funcP)()=&A::func;

To specify the const version, you must indicate const in the declaration:

1
void(A::*funcConstP)()const=&A::func;

For const A objects, use the const version, and for non-const A objects, use the non-const version; they cannot be mixed.

1
2
3
4
5
6
7
const A x;
(x.*funcP)(); // ERROR!
(x.*funcConstP)(); // OK

A y;
(y.*funcConstP)(); // ERROR!
(y.*funcP)(); // OK

Compare Operation Implementation in STL

Unlike C language macros, utilizing templates and predicates in C++ allows writing generic compare operations easily.
In a macro definition, care must be taken regarding parameter side effects, as macros are merely replacements, such as:

1
2
3
4
5
#define MAX(a,b) a>=b?a:b;

MAX(--a,++b);
// Replaced with
--a>=++b?--a:++b;

But this macro’s actual operation does not yield the behavior we expect.
Fortunately, in C++, we can avoid such ugly macro definitions using templates and also provide a custom predicate to realize our judgment behavior:

1
2
3
4
5
6
7
8
9
10
11
12
struct Compare{
template<typename T>
bool operator()(const T& a,const T& b){
return a<b?false:true;
}
};

template<class T, class Compare>
const T& max(const T& a, const T& b, Compare comp)
{
return (comp(a, b)) ? b : a;
}

Computational Constructor

In certain cases, creating a constructor can improve the execution efficiency of member functions.

1
2
3
4
5
6
7
8
9
10
11
12
struct String{
String(const char* init);
const String operator+(const String& l,const String& r){
return String(l.s_,r.s_);
}
private:
String(const char* a,const char* b){
s_=new char[strlen(a)+strlen(b)+1];
strcat(strcpy(s_,a),b);
}
char *s_;
};

Using Members of Its Own Type

How can a member of a class access the current class type?
This can be written as follows:

1
2
3
4
5
6
7
template<typename T>
struct base{
using selfType=T;
};

template<typename T>
struct foo:public base<foo<T>>{};

Though it has a somewhat forced feel…

Random Access in std::vector

std::vector allows random access because it overloads the [] operator and has at member function, thus typically we would have the following two ways:

1
2
3
4
5
template<typename T>
void f(std::vector<T>& x){
x[0];
x.at(0);
}

What are the differences between these two random access methods?

Sequential container’s at(size_type) requires range checks.
[ISO/IEC 14882:2014] The member function at() provides bounds-checked access to container elements. at() throws out_of_range if n >= a.size().
However, the standard doesn’t impose any requirements on operator[].

Let’s take a look at some STL implementations (SGISTL) to see how std::vector implements operator[size_type] and at(size_type):
First, the implementation of at(size_type):

1
2
3
4
5
6
7
8
9
10
11
12
// Implementation of at(size_type)
#ifdef __STL_THROW_RANGE_ERRORS
void _M_range_check(size_type __n) const {
if (__n >= this->size())
__stl_throw_range_error("vector");
}

reference at(size_type __n)
{ _M_range_check(__n); return (*this)[__n]; }
const_reference at(size_type __n) const
{ _M_range_check(__n); return (*this)[__n]; }
#endif /* __STL_THROW_RANGE_ERRORS */

Now look at the implementation of operator[](size_type):

1
2
3
// Implementation of operator[](size_type)
reference operator[](size_type __n) { return *(begin() + __n); }
const_reference operator[](size_type __n) const { return *(begin() + __n); }

As you can see, there is no range check in the random access of operator[].
Thus the questions:

1
2
x[0];
x.at(0);

These two differ in that if x is non-empty, the behavior is the same; if x is empty, x.at(0) would throw an std::out_of_range exception (as required by C++ standards), while x[0] would lead to undefined behavior.

Note the Difference Between typedef and #define

1
2
3
4
5
6
7
8
typedef int* INTPTR;
#define INTPTR2 int*
int main(int argc,char* argv[])
{
INTPTR i1,i2;
INTPTR2 i3,i4;
return 0;
}

Let’s directly see the IR code:

1
2
3
4
%6 = alloca i32*, align 8
%7 = alloca i32*, align 8
%8 = alloca i32*, align 8
%9 = alloca i32, align 4

Note that %9 is not i32*, it’s an i32 object.
Because #define is merely a simple replacement at compile time, it expands during compilation as follows:

1
2
3
4
#define INTPTR2 int*
INTPTR2 i3,i4;
// Compile-time expansion
int* i3,i4;

Thus, only i3 is of type int*, while i4 is of type int.

Why const Object Is Not a Compile-Time Constant?

1
2
const int x=10;
int y[x]={0};

This is permissible; during compiler optimization, x will be replaced directly with 10.
The intermediate code is as follows:

1
2
3
4
5
%6 = alloca i32, align 4
%7 = alloca [10 x i32], align 16
store i32 10, i32* %6, align 4
%8 = bitcast [10 x i32]* %7 to i8*
call void @llvm.memset.p0i8.i64(i8* %8, i8 0, i64 40, i32 16, i1 false)

It can be seen that %7’s allocation does not use %6, hence it does not depend on x, and this object is known at compile-time.
However, consider when we write:

1
2
3
4
5
int x;
cin>>x;
const int y=x;
// error: variable-sized object may not be initialized
int z[y]={0};

Here it’s because of compiler extensions, so C++ also supports VLA. But we can see that const cannot serve as a compile-time constant.

Class Query in Inheritance Hierarchies

In class inheritance hierarchies, several different derived classes may have the same base class; they might mutually inherit, resulting in several inherited levels. How do we check whether a certain derived class in a hierarchy inherits from a certain class?

We can use dynamic_cast to achieve our requirement. For an overview of C++ type conversions, you can check out my previous article: A Detailed Analysis of Type Conversions in C++. Here’s a description of dynamic_cast in the C++ standard (ISO/IEC 14882:2014):

The result of the expression dynamic_cast<T>(v) is the result of converting the expression v to type T. T shall be a pointer or reference to a complete class type, or “pointer to cv void.” The dynamic_cast operator shall not cast away constness (5.2.11).

If C is the class type to which T points or refers, the runtime check logically executes as follows:

  • If, in the most derived object pointed (referred) to by v, v points (refers) to a public base class subobject of a C object, and if only one object of type C is derived from the subobject pointed (referred) to by v, the result points (refers) to that C object.
  • Otherwise, if v points (refers) to a public base class subobject of the most derived object, and the type of the most derived object has a base class of type C that is unambiguous and public, the result points (refers) to the C subobject of the most derived object.
  • Otherwise, the runtime check fails.

The value of a failed cast to pointer type is the null pointer value of the required result type. A failed cast to reference type throws an exception (15.1) of a type that would match a handler (15.3) of type std::bad_cast (18.7.2).

Thus, we can execute dynamic_cast conversion on class pointers in inheritance hierarchies to check the conversion success, hence determining whether a certain class exists in a derived class hierarchy.
Here’s a code example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

struct Shape{
virtual void draw()=0;
virtual ~Shape(){}
};
struct Roll{
virtual void roll(){cout<<"Roll:roll()"<<endl;}
virtual ~Roll(){}
};
struct Circle:public Shape,public Roll{
void draw(){
cout<<"Circle::draw"<<endl;
}
void roll(){
cout<<"Circle::roll()"<<endl;
}
~Circle()=default;
};

struct Square:public Shape{
void draw(){
cout<<"Square::draw()"<<endl;
}
~Square()=default;
};

int main(int argc,char* argv[])
{
Shape *a=new Square;
Roll *b=dynamic_cast<Roll*>(a);
if(b!=NULL){
cout<<"yes"<<endl;
}else{
cout<<"no"<<endl;
}

delete a;
return 0;
}
// output: no

From the code above, you can see the inheritance hierarchy of the Circle class:

And the inheritance hierarchy of the Square class:

The above inheritance hierarchies are quite simple, but if we assume we are unaware of the specific inheritance hierarchies of Circle and Square, how do we determine whether Square contains a certain base class (like Roll)?
The solution, as mentioned above, is to utilize dynamic_cast! By converting to the class type that we want to check for in the hierarchy, if the conversion is successful, dynamic_cast returns a pointer converted from the source type to the target type; if it fails, it will return a null pointer (the reason for not using references is that we want to handle the potential threat of exceptions). This conversion is neither an upward nor downward conversion, but rather a lateral conversion. Hence, we need to check the object (pointer) returned by dynamic_cast to ascertain whether the detected type exists in the inheritance hierarchy.

However, I feel that this behavior applies only to very narrow scenarios and is rarely necessary in well-designed classes; if you feel overwhelmed by your class hierarchy, it is surely a poor design.

In list initialization, the initializer_list constructor takes precedence over the ordinary constructor

First, let’s introduce two basic concepts:

List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is called an initializer list.
initializer-list constructor: A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list<E> or reference to possibly cv-qualified std::initializer_list<E> for some type E, and either there are no other parameters or else all other parameters have default arguments (8.3.6).

Note: Initializer-list constructors are favored over other constructors in list-initialization (13.3.1.7).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <initializer_list>

class A{
public:
A(int,int){
std::cout<<"A(int,int)"<<std::endl;
}
A(initializer_list<int>){
std::cout<<"A(initializer_list<int>)"<<std::endl;
}
};

int main()
{
A a{1,2}; // A(initializer_list<int>)
A b(1,2); // A(int,int)
return 0;
}

[ISO/IEC 14882:2014 13.3.1.7 Initialization by list-initialization] When objects of non-aggregate class type T are list-initialized (8.5.4), overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the
    argument list consists of the initializer list as a single argument.
  • If no viable initializer-list constructor is found, overload resolution is performed again, where the
    candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

If the initializer list has no elements and T has a default constructor, the first phase is omitted. In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed. [ Note: This differs from other situations (13.3.1.3, 13.3.1.4), where only converting constructors are considered for copy-initialization. This restriction only applies if this initialization is part of the final result of overload resolution. - end note ]

In simpler terms, when the constructor parameters of a class are list-initialized, the overload resolution of the constructor is divided into two steps:

  1. First, it tries to match with the constructor whose parameter is an initializer-list. If the initializer-list has no elements and the class has a default constructor, this step is omitted.
  2. If no viable initializer-list constructor is found, overload resolution is performed again, with all constructors of the class as candidates, and the argument list consists of the elements of the initializer-list.

Note: In the context of copy list initialization (copy-initializer_list), if an explicit constructor is selected, the program becomes ill-formed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A{
public:
A(int,int)
{
cout<<"A(int,int)"<<endl;
}
explicit A(std::initializer_list<int>)
{
cout<<"explicit A(std::initializer_list<int>)"<<endl;
}
};

int main()
{
A a={1,2}; // error: chosen constructor is explicit in copy-initialization
}

Moreover, explicit constructors will disable the implicit conversion of the initializer list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A;
void f(const A&){}

class A{
public:
A()
{
cout<<"A()"<<endl;
}
explicit A(std::initializer_list<int>)
{
cout<<"explicit A(std::initializer_list<int>)"<<endl;
}
};

int main()
{
f({1,2}); // ERROR due to explicit (no implicit type conversion allowed)
f(A{1,2}); // OK
}
The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:Programming skills and concepts in C/C++
Author:LIPENGZHA
Publish Date:2017/03/05 01:21
Update Date:2017/04/28 07:53
Word Count:19k Words
Link:https://en.imzlp.com/posts/1756/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!