Detailed analysis of type conversion in C++

详细分析C++中的类型转换

In C++, when the operand types of an operator are inconsistent, these operands will be converted to the same type. Type conversion is divided into implicit conversion and explicit conversion.

First, let’s understand the meaning of built-in types in C++ and their standard defined sizes:

Type Meaning Minimum Size
bool Boolean Type Undefined
char Character 8 bits
wchar_t Wide Character 16 bits
char16_t Unicode Character 16 bits
char32_t Unicode Character 32 bits
short Short Integer 16 bits
int Integer 16 bits
long Long Integer 32 bits
long long Long Long Integer 64 bits
float Single Precision Floating Point 6 Significant Digits
double Double Precision Floating Point 10 Significant Digits
long double Extended Precision Floating Point 10 Significant Digits

The following code can be used to detect the sizes of built-in types across different systems (32-bit and 64-bit):

1
2
3
4
5
6
7
8
9
10
11
12
printf("%11s\t%3d byte\t%3d bit\n","bool",sizeof(bool),sizeof(bool)*8);
printf("%11s\t%3d byte\t%3d bit\n","char",sizeof(char),sizeof(char)*8);
printf("%11s\t%3d byte\t%3d bit\n","wchar_t",sizeof(wchar_t),sizeof(wchar_t)*8);
printf("%11s\t%3d byte\t%3d bit\n","char16_t",sizeof(char16_t),sizeof(char16_t)*8);
printf("%11s\t%3d byte\t%3d bit\n","char32_t",sizeof(char32_t),sizeof(char32_t)*8);
printf("%11s\t%3d byte\t%3d bit\n","short",sizeof(short),sizeof(short)*8);
printf("%11s\t%3d byte\t%3d bit\n","int",sizeof(int),sizeof(int)*8);
printf("%11s\t%3d byte\t%3d bit\n","long",sizeof(long),sizeof(long)*8);
printf("%11s\t%3d byte\t%3d bit\n","long long",sizeof(long long),sizeof(long long)*8);
printf("%11s\t%3d byte\t%3d bit\n","float",sizeof(float),sizeof(float)*8);
printf("%11s\t%3d byte\t%3d bit\n","double",sizeof(double),sizeof(double)*8);
printf("%11s\t%3d byte\t%3d bit\n","long double",sizeof(long double),sizeof(long double)*8);

Implicit Type Conversion

In C++, three situations trigger (implicit) type conversion:

  1. In a mixed-type expression, the operands are converted to the same type (integer promotion).
  2. When used as a conditional expression, they are converted to bool (non-zero becomes true).
  3. When an expression initializes a variable or assigns a value to a variable, the value of that expression is converted to the type of that variable.

The basic rules for (implicit) type conversion in C++ are:

  • Integer Promotion: Converts smaller integer types to larger integer types.
  • Truncation: When converting from higher precision to lower precision, truncation occurs (e.g., converting from float to int discards the decimal part).
  • Arithmetic types to bool: Non-zero becomes true, while all others become false.
  • Conversion between signed and unsigned: This has side effects (e.g., assigning a negative value to an unsigned type).

Integer Promotion

For types such as bool, char, signed char, unsigned char, short, and unsigned short, they will be promoted to int if all their possible values can fit in int, otherwise, they will be promoted to unsigned int.

Larger char types (wchar_t, char16_t, char32_t) are promoted to the smallest type among int, unsigned int, long, unsigned long, long long, and unsigned long long, provided that the converted type must be able to hold all possible values of the original type.

If all possible values of a bit-field can be represented by int, it is converted to int; otherwise, if all values can be represented by unsigned int, it is converted to unsigned int; if neither int nor unsigned int can represent all values, no integer promotion is performed.
Bit-field: You can specify the number of bits occupied by members using a struct to define it as a bit-field.

1
2
3
4
5
6
7
8
9
10
// A 32-bit bit-field example
struct PPN{
unsigned int PFN:22;
int:3; // Unused bits
unsigned int CCA:3;
bool nonreachable:1;
bool dirty:1;
bool valid:1;
bool global:1;
};

The bool value converts to int, where false becomes 0 and true becomes 1.

Conversion of Floating Point Types

For floating-point operations and rounding, you can refer to these two articles: IEEE 754 and Floating-point rounding in two iterator initialization containers.

Given a floating-point value, we can convert it to other floating-point types. If the original value can be completely represented by the target type, the resultant value is equal to the original value. If the original value lies between two adjacent target values, one of them is taken. In other cases, the result is undefined.

1
2
3
4
5
double d1=DBL_MAX;  // The largest double precision floating-point value
float f1=d1; // If DBL_MAX > FLT_MAX, the result is undefined.

long double ld=numeric_limits<long double>::max();
double d1=ld; // If sizeof(long double) > sizeof(double), the result is undefined.

Conversion from high precision to low precision involves truncation.

1
2
3
double dPI=3.14159;
// iPI results in 3, truncation occurs
int iPI=dPI;

Conversion from low precision to high precision involves widening.

1
2
3
int ival=3;
// fval becomes 3.00000
float fval=ival;

Additionally, earlier versions of C++ allowed rounding up or down for results to be negative, while the C++11 standard stipulates that division is always truncated toward zero (i.e., truncation -> direct removal of the decimal part).

1
2
3
4
int x=-12;
int y=5;
// Result is -2
cout<<x/y<<endl;

Using numeric_limits ensures that truncation occurs in a portable manner. During initialization, the {} initializer helps prevent truncation.

Conversion Between Unsigned Types

If the target type is unsigned, the resulting value occupies the same number of bits as the target type (losing leading bits if necessary). More accurately, the value of the integer before conversion is the result of taking the modulus with respect to $2^n$, where n is the number of bits the target type occupies.

1
2
// In binary 1111111111: uc's value becomes binary 111111, which is 255
unsigned char uc=1023;

If the target type is signed, when the original value can be represented by the target type, it remains unchanged; otherwise, the resulting value depends on the specific implementation:

1
2
// Depends on the implementation, result is 127 or -1
signed char sc=1024;

A bool value or a regular enum value can be implicitly converted to an equivalent integer type.

If an operand is of an unsigned type, then the result of the conversion will depend on the relative sizes of the integer types in the machine.

Because the standard specifies that int is not less than short, long is not less than int, and long long is not less than long, there is a possibility that int (default is signed) may not fit unsigned short or long may not fit unsigned int, and long long may not fit unsigned long. Special care should be taken in such cases.

When an expression contains short and int, short is converted to int.

When the int type can adequately represent all unsigned short values, unsigned short is converted to int; otherwise, both operands will be converted to unsigned int.

Converting from unsigned int to long and from unsigned long to long long follows the same principle.

In addition, note the following:

  • Assign a value to an unsigned type: The result is the initial value modulo the total number of values that the unsigned type can represent $2^n$ (for example, unsigned char can contain values from 0 to 255, so it can represent 256 values).
  • Converting from signed (default) type to unsigned type may cause side effects (assigning a negative value to unsigned).
  • Assigning a value outside its range to a signed type: The result is undefined.
1
2
3
4
unsigned char x=258;
// out:2
cout<<x;
// The result is the value of 258 % 256

Assigning a negative value to an unsigned type
Negative values in computers involve the concept of two’s complement; for detailed explanations, refer to these articles: IEEE 754: Binary Floating-Point Arithmetic Standard, About Two’s Complement, Original Code, One’s Complement, Two’s Complement Explained.

We will briefly cover the basics of the concepts of original code, one’s complement, and two’s complement:

Original code: The method for handling sign and magnitude is to allocate a sign bit to represent the sign: setting this bit (usually the most significant bit) to 0 denotes a positive number, and 1 denotes a negative number.

One’s complement: The binary representation of a number in one’s complement form is achieved by inverting its magnitude (the sign bit remains the same, but all other bits are inverted).

Two’s complement: When a number is greater than or equal to 0, its two’s complement remains the same as its original code; if the number is less than zero, it is obtained by keeping the sign bit the same while inverting all other bits, and then adding 1 (i.e., adding 1 to the one’s complement).

Signed data is typically encoded in computers using two’s complement, as both original and one’s complement representations have two forms for zero: -0 and +0.

Let’s test assigning a negative number to an unsigned type:

1
2
3
4
// To make things easier, we'll use a 1-byte char here
unsigned char x=-2;
// Output the value of character x (since there are many null characters in char, direct output sometimes can't intuitively show test results)
cout<<(int)x;

After compiling and running, the result is: 254.

Since a number’s two’s complement is the way it is stored in a computer, assigning a negative value to an unsigned type causes the sign bit to act as a value. Thus, the actual value stored after assigning a negative value to an unsigned type is the total number of values it can represent minus the absolute value of that negative number.

To illustrate assigning -2 to an unsigned char:

Let’s test other types:

Assuming that an int variable is 4 bytes (32 bits) on one platform, it can represent a range from [−2147483648 ($-2^{31}$), 2147483647 ($2^{31}$-1)].

Let’s choose a number: −111.

[Original] 1 0000000000000000000000001101111
[One’s complement] 1 1111111111111111111111110010000
[Two’s complement] 1 1111111111111111111111110010001

We will estimate that assigning this to an unsigned int variable yields a value of 4294967185.

Let’s verify with a custom base conversion wheel:

binary 11111111111111111111111110010001 converts to decimalism is:4294967185

1
2
unsigned int x = -111;
cout<<x<<endl;

Run the result:

Arrays Converting to Pointers

In most expressions involving arrays, the array automatically converts to a pointer to the first element of the array.

1
2
3
4
// An array containing 10 int elements
int ia[10];
// ia converts to point to the first element of the array
int *ip=ia;

When an array is used as an argument of the decltype keyword, or as operands for operators such as address-of (&), sizeof, and typeid, the above conversion does not occur.

Similarly, if an array is initialized with a reference, the above conversion will not occur.

1
2
3
int *ia[10];
// ip points to an array containing 10 int elements
int (*ip)[10]=&ia;

Pointer Conversions

C++ also specifies several other ways for pointer conversions.

  1. Constant integer value 0 or literal nullptr (C++11 feature) can convert to any pointer type.
  2. A pointer to any non-constant can convert to void*.
  3. A pointer to any object can convert to const void*.
  4. Pointer conversion among classes with inheritance: A pointer or reference to a derived class object can be used in places where a base class reference is required, and a pointer to a derived class object can be used in places where a base class pointer is required.

Note: Pointers to functions and pointers to members cannot be implicitly converted to void*, and there is no conversion from pointer types to numeric types.

Constant expressions that evaluate to 0 can implicitly be converted to any pointer type, including member pointer types. For example:

1
int *p=(1+2)*(2*(1-1)); // Correct, but curious

It’s best to directly use nullptr.

T* can implicitly be converted to const T*, and similarly T& can implicitly be converted to const T&.

Pointer Conversions Between Inherited Classes

Pointer conversions between classes with inheritance have another way:

1
2
3
4
5
Base item; // Base class object
Derived bulk; // Derived class object
Base *p=&item; // p points to Base object
p=&bulk; // p points to the Base part of bulk
Base &r=bulk; // r binds to the Base part of bulk

This conversion is known as derived to base type conversion. Like other type conversions, the compiler will implicitly perform the conversion from derived class to base class.
A pointer (or reference) to a derived class can implicitly convert to a pointer (or reference) to its accessible and unambiguous base class.

This implicit nature means:

  • You can use derived class objects or references to derived class objects in places that require base class references.
  • You can also use pointers to derived class objects in places that require base class pointers.

Derived class objects contain components corresponding to their base class, this fact is the key to inheritance.

Conversion of bool Types

Pointers, integers, and floating points can be implicitly converted to bool types. Non-zero values correspond to true, and zero values correspond to false.

When a pointer is implicitly converted to bool, it is false only when it is nullptr/NULL.

1
2
3
4
5
6
7
8
9
10
11
12
int *p=nullptr;
if(p){
cout<<"true"<<endl;
} else {
cout<<"false"<<endl;
}
int *ivalp=new int(0);
if(ivalp){
cout<<"true"<<endl;
} else {
cout<<"false"<<endl;
}

When converting a non-bool value to a bool value, only the initial value of 0 results in false, otherwise it will be true.

1
2
3
4
5
6
7
// true only when test=0; true when test_bool>0 or test_bool<0
bool test_bool = 11;
if(test_bool){
cout<<"True!"<<endl;
} else {
cout<<"False"<<endl;
}

Assigning a bool type value to a non-bool type will result in 0 if the initial value is false, and 1 if the initial value is true.

1
2
3
4
int boolToInt_1=true;
int boolToInt_2=false;
// Output: 1 0
cout<<boolToInt_1<<"\t"<<boolToInt_2<<endl;

Conversion of Enum Types

C++ automatically converts enum members (enumerator) to integers, and the conversion result can be used wherever an integer value is required.

1
2
3
4
5
// point2d is 2, point2w is 3, point3d is 3, point3w is 4
enum Points{point2d=2, point2w, point3d=3, point3w};
// Can also use auto (C++11) to infer the smallest type that can hold point3d
int array3d=point3d;
cout<<array3d<<endl;

Note the following points:

  • The type to which an enum object or enum member is promoted is machine-defined (depends on whether the machine type can accommodate it) and depends on the maximum value of the enum member.
  • Enum objects or members are at least promoted to int.
  • If the int type cannot represent the maximum value of the enum member (using auto to infer the type that can hold the enum member), it is promoted to the smallest type that can represent all enum member values and is larger than int (unsigned int, long, unsigned long, long long, unsigned long long).

Conversion to Constant (const) Objects

  • When a non-const object is used to initialize a const reference, the system will convert the non-const object to a const object.

  • You can also convert a pointer to a non-const object (or non-const pointer) to a pointer to a const object.

  • It is not allowed to convert a const object to a non-const object.

1
2
3
4
5
6
7
8
int i=10;
const int &x=i;
// Output 10
cout<<x<<endl;
// i can be modified, but x cannot be modified
i=12;
// Output 12
cout<<x<<endl;

A non-const pointer becomes a const pointer:

1
2
3
4
5
6
7
8
9
10
int i=10;
cout<<&i<<endl;
const int *x=&i;
// i can be modified, but the value pointed to by x cannot be modified
i=12;
// *x=13 This is incorrect
//error: read-only variable is not assignable
cout<<i<<"\t"<<x<<" is "<<*x<<endl;
const int *z=x;
cout<<x<<"\t"<<z<<endl;

If T is a type, we can convert a pointer or reference to T into a pointer or reference to const T.

1
2
3
4
5
6
7
8
9
int i;
// Convert a non-const to a const int reference
const int &j=i;
// The address of the const object becomes the const pointer
const int *p=&i;
// Error: Converting const to non-const is not allowed
int &r=j,*p=p;
// error: binding value of type 'const int' to reference to type 'int' drops 'const' qualifier
// error: cannot initialize a variable of type 'int *' with an lvalue of type 'const int *'

The reverse conversion does not exist, as it attempts to remove the underlying const.

Explicit Type Conversion

Sometimes we wish to explicitly convert an object to another type (for example, if the result of dividing two integers should also be of integer type, discarding the precision after the decimal point, which sometimes is not the desired result), we need to use explicit type conversion. C++’s explicit type conversion operations specify several different capabilities of conversion operations for stronger controls over conversion privileges. Although all four of these conversion operations can be accomplished using C-style conversion, it is safer to use explicit type conversion.

C-Style Explicit Type Conversion

In C, the casting operator (type) is used to force conversion of types when needed.

Refer to the following code:

1
2
3
int x=12,y=5;
float z=(float)x/y;
printf("%f\n",z);

If we do not use the casting operator, the output of z would be 2.000000; by using the casting operator, x is first converted to float type, and then division is performed. Since the implicit type conversion of different types within the same expression goes from lower precision to higher precision, y will also be implicitly converted to float, resulting in: 2.400000.

Earlier versions of C++ also supported the type(expression) method for type conversion.

Explicit Type Conversion in C++

C++ does support C-style explicit type conversions (casting operator), but there are better alternatives in C++. C++ offers several different ways to perform explicit type conversions.

A named cast has the following form:

1
cast-name<type>(expression);

Where cast-name determines the type of conversion being executed, type is the target type, and expression is the value to be converted.

  • If type is a reference type, the result is an lvalue.

  • Cast-name can be one of static_cast, dynamic_cast, const_cast, or reinterpret_cast.

static_cast

The role of static_cast is to reverse a well-defined implicit type conversion.

Any type conversion with a clear definition that does not involve underlying const (indicating that the object pointed to by the pointer is a constant) can use static_cast.

Note: static_cast cannot handle pointer conversions, as pointer conversions are bit-pattern conversions and should use reinterpret_cast (to disguise as another type of pointer):

1
2
3
4
char x='a';
int *p1=&x; // Error, no implicit conversion from char* to int*
int *p2=static_cast<int*>(&x); // Error, no implicit conversion from char* to int*
int *p3=reinterpret_cast<int*>(&x); // OK, at your own risk

Where it applies:

  1. Floating point precision loss (double -> float) and assigning a larger arithmetic type to a smaller arithmetic type (int -> char), using static_cast signifies that we understand and do not care about these risks. :)
  2. Type conversions that the compiler cannot automatically carry out *(void -> otherType)**.
  3. When the compiler detects that a larger data type is trying to be assigned to a smaller data type, it will raise a warning; using explicit type conversion will suppress the warning.

Refer to the following code:

1
2
3
4
5
int x=12,y=5;
// Use static_cast to forcibly convert int x to double before computing
float z=static_cast<double>(x)/y;
// Output result 2.4
cout<<z<<endl;
1
2
3
double dPI=3.14159;
// iPI is 3.
int iPI=static_cast<int>(dPI);
1
2
int x=65;
char capital_a=static_cast<char>(x);

Using static_cast to retrieve an existing *void** pointer:

void* is a special pointer type that can store the address of any non-constant object.

However, the operations that void can perform are very limited: comparing it with other pointers, using it as an input or output to functions, and assigning it to another void* object. We cannot directly manipulate a void* pointer because we do not know what type this object is and what operations can be performed on it.

Therefore, when we have a void* whose object type is known, but we are still unable to operate on that void type, the current approach is to use static_cast to convert void* back to the known pointer type the void* points to.

1
2
3
4
5
double dval=3.1415926;
// Assuming we have a void* and vpoint could be a function return value or another pointer type that is not directly visible
void *vpoint=&dval;
// We can use static_cast to convert void* to double*
double *p=static_cast<double*>(vpoint);

When we store a pointer in void* and use static_cast to forcefully convert it back to its original type, we must ensure that the pointer’s value remains unchanged. In other words, the result of the cast must equal the original address value.

1
2
// Output the address of dval and the address of the pointer after forcibly converting void* back to double*
cout<<&dval<<"\t"<<p<<endl;

Both addresses output the same.

Thus, we must ensure that the resulting type matches the type of the pointer being pointed to. If the types do not match, it will result in an undefined error (not knowing what kind of result will occur).

const_cast

The role of const_cast is to grant write access to certain objects declared as const.

const_cast can only change the underlying const (the object pointed to by the pointer is a constant), which means making the object pointed to by a constant pointer modifiable.

1
2
const char *pchar;
char *p=const_cast<char*>(pchar);

The act of converting a constant object into a non-constant is termed “removing the const nature (cast away the const)”. Once a const nature is removed from an object, the compiler does not prevent operations that modify this object.

If the object itself is not a constant, it is legal to use a cast to obtain write permission. If the object is a constant, attempting to perform write operations using const_cast would result in undefined behavior.

The underlying const object itself is a variable:

1
2
3
4
5
6
7
char a='a';
const char *pchar=&a;
char *p=const_cast<char*>(pchar);
// Modify the object pointed to by p (i.e., the char object a) to b
*p='b';
// Output b
cout<<a<<endl;

The underlying const object itself is a constant:

1
2
3
4
5
6
char *a="HelloWorld"; 
const char *pchar=a;
char *p=const_cast<char*>(pchar);
// Compilation passes, but results in undefined behavior
*p='b';
cout<<a<<endl;

const_cast is particularly useful in overloaded functions.

When our function accepts a const reference of an object and returns a reference to the modified object, since the parameter is declared as const, the return value will also have const properties, which obviously isn’t always the result we want.

Assuming we need a function that compares and returns the longer of two strings, we may have the following code:

1
2
3
4
// The function's parameter and return value are both const string
const string& longString(const string &s1,const string &s2){
return s1.size()>s2.size()?s1:s2;
}

When we call longString with two non-const string arguments, the return value will still be a const reference to string. So we need a new longString function, and when its arguments are not constants, we wish for the result to be a regular reference, using const_cast can achieve this.

1
2
3
4
5
6
7
8
// Overloaded longString function, which calls this version when parameters are non-const, returning a non-const object
string &longString(string &s1,string &s2){
// Call the version that accepts two const parameters and returns a const object
// r's type is const string&
auto &r=longString(const_cast<const string&>(s1),const_cast<const string&>(s2));
// Use const_cast to convert r from const string& to string&
return const_case<string&>(r);
}

reinterpret_cast

reinterpret_cast usually provides a lower-level reinterpretation of the bit patterns of the operands (a function for changing bit patterns).

The following code:

1
2
int *ipoint;
char *cpoint=reinterpret_cast<char*>(ipoint);

We must keep in mind that cpoint points to an int, not a char; using cpoint as a regular char pointer can lead to run-time errors.

1
2
// This doesn't report an error, but may lead to various problems during execution.
string str(cpoint);

If we initialize the string object with ipoint without using reinterpret_cast, it will throw an error:

1
2
3
4
int *ipoint;
// char *cpoint=reinterpret_cast<char*>(ipoint);
string str(ipoint);
// Compilation error: error: no matching constructor for initialization of 'string' (aka 'basic_string<char>')

Using reinterpret_cast is very dangerous. As shown in the examples, the problem is that the type is changed, but the compiler doesn’t warn or report errors. Since explicitly stating this action is legal, the compiler will not issue any warnings or errors. After using reinterpret_cast<char*>(ipoint);, when we use cpoint, it will consider it a char* type, and the compiler cannot know that it actually stores a pointer to int.

You can also manually specify an address to convert to a specified type pointer:

1
int *p=reinterpret_cast<int*>(0xff00);

Note: The compiler cannot determine whether the integer 0xff00 is a valid int type address; hence the correctness of this statement depends entirely on the programmer.

dynamic_cast

dynamic_cast: Checks class hierarchy dynamically.
The role of dynamic_cast is to: Safely convert a base class pointer or reference to a derived class (inherited class) pointer or reference.

The usage format for dynamic_cast is as follows:

1
2
3
4
5
6
// e must be a valid pointer
dynamic_cast<type*>(e);
// e must be an lvalue
dynamic_cast<type&>(e);
// e cannot be an lvalue
dynamic_cast<type&&>(e);

Where type must be a class type, and typically that type has virtual functions.

In the three forms of dynamic_cast, the type of e must meet at least one of the following three conditions:

  • The type of e is a public derived class of the target type.
  • The type of e is a public base class of the target type.
  • The type of e is the type of the target type.

If any of these conditions are met, the conversion succeeds; otherwise, it fails.

If the dynamic_cast conversion target is of pointer type and fails, the result is 0.

If the dynamic_cast conversion target is of reference type and fails, the dynamic_cast operator will throw a bad_cast exception.

Pointer Type dynamic_cast

Assuming the Base class has at least one virtual function, and Derived is a public derived type of Base, if we have a pointer to Base bp, then we can convert it to a pointer to Derived at runtime.

1
2
3
4
5
6
7
// Check if the conversion is successful; pointer bp cannot be accessed outside of the if
// The dynamic_cast operation in the condition part ensures that the type conversion and result check occur in the same expression
if(Derived *dp = dynamic_cast<Derived*>(bp)){
// Use the Derived object pointed to by dp
} else { // bp points to a Base object
// Use dp to reference the Base object
}

If bp points to a Derived object, the type conversion initializes dp and points it to the Derived object pointed to by bp. At this point, using dp safely refers to Derived operations. Otherwise, the type conversion results in 0, meaning the condition of the if statement fails, and the else clause executes the relevant Base operations.

You can execute dynamic_cast on a null pointer, which will yield a null pointer of the desired type.

Reference Type dynamic_cast

The reference type dynamic_cast differs from the pointer type dynamic_cast in terms of error reporting.

Because there is no null reference, it cannot use the same error reporting strategy as pointer types. When the type conversion fails for reference types, the program throws an exception named std::bad_cast, which is defined in the typeinfo standard header.

Rewrite the program above to use reference types:

1
2
3
4
5
6
7
8
void f(const Base &b){
try{
const Derived &d=dynamic_cast<const Derived&>(b);
// Use the Derived object referenced by b
} catch (bad_cast) {
// Handle the type conversion failure
}
}
The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:Detailed analysis of type conversion in C++
Author:LIPENGZHA
Publish Date:2016/05/04 19:35
Word Count:17k Words
Link:https://en.imzlp.com/posts/27258/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!