Modern C++ conversion constructors and type conversion functions

In C/C++, different data types can be converted to each other. The type that does not require the user to specify how to convert is called automatic type conversion (implicit type conversion), and the type that requires the user to explicitly specify how to convert is called forced type conversion.

Whether it is automatic type conversion or forced type conversion, the prerequisite must be that the compiler knows how to convert. For example, converting a decimal to an integer will erase the numbers after the decimal point (from wide to narrow), and convert int * Converting to float * simply copies the value of the pointer. These rules are built in by the compiler and we don’t tell the compiler.

C++ allows us to customize type conversion rules. Users can convert other types to the current class type, or convert the current class type to other types< /strong>. This custom type conversion rule can only appear in the form of a member function of a class. In other words, this conversion rule only applies to custom classes.

Conversion constructor:

Converting other types to the current class type requires the use of the Conversion constructor. A conversion constructor is alsoa constructor that follows the general rules for constructors. The conversion constructor has only one parameter.

#include <iostream>
using namespace std;

// plural class
class Complex{
public:
    Complex(): m_real(0.0), m_imag(0.0){ }
    Complex(double real, double imag): m_real(real), m_imag(imag){ }
    Complex(double real): m_real(real), m_imag(0.0){ } // Conversion constructor
public:
    friend ostream & amp; operator<<(ostream & amp;out, Complex & amp;c); // Friend function
private:
    double m_real; // real part
    double m_imag; // imaginary part
};

// Overload >> operator
ostream & amp; operator<<(ostream & amp;out, Complex & amp;c){
    out << c.m_real <<" + "<< c.m_imag <<"i";;
    return out;
}

int main(){
    Complex a(10.0, 20.0);
    cout<<a<<endl;
    a = 25.5; // Call the conversion constructor
    cout<<a<<endl;
    return 0;
}

operation result:
10+20i
25.5+0i

Complex(double real) is the conversion constructor. Its function is to convert the double type parameter real into an object of the Complex class, using real as the real part of the complex number and 0 as the imaginary part of the complex number. In this way, the overall effect of a = 25.5 is equivalent to: a.Complex(25.5); converts the assignment process into a function call process.

When performing mathematical operations, assignments, copies, etc., if you encounter type incompatibility and need to convert the double type to the Complex type, the compiler will search whether the current class defines a conversion constructor. If it is not defined, the conversion will fail. , if defined, calls the conversion constructor.

The conversion constructor is also a type of constructor. In addition to converting other types to the current class type, it can also be used to initialize objects. This is the original meaning of the constructor.

It should be noted that in order to obtain the target type, the compiler will do whatever it takes to obtain the target type. It will use a combination of built-in conversion rules and user-defined conversion rules, and will perform multi-level type conversions, such as:

  • The compiler will first convert int to double according to built-in rules, and then convert double to Complex according to user-defined rules (int –> double –> Complex);
  • The compiler will first convert char to int according to built-in rules, then convert int to double, and finally convert double to Complex according to user-defined rules (char –> int –> double –> Complex).
int main(){
    Complex c1 = 100; // int --> double --> Complex
    cout<<c1<<endl;
    c1 = 'A'; // char --> int --> double --> Complex
    cout<<c1<<endl;
    c1 = true; // bool --> int --> double --> Complex
    cout<<c1<<endl;
    Complex c2(25.8, 0.7);

    // Assume the + operator has been overloaded
    c1 = c2 + 'H' + true + 15; // Convert char, bool, and int to Complex type and then perform operations
    cout<<c1<<endl;
    return 0;
}

operation result:
100+0i
65+0i
1+0i
113.8 + 0.7i

You can use the default parameters of the constructor to reduce the number of constructors.

#include <iostream>
using namespace std;

// plural class
class Complex{
public:
    Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
public:
    friend ostream & amp; operator<<(ostream & amp;out, Complex & amp;c); // Friend function
private:
    double m_real; // real part
    double m_imag; // imaginary part
};

//Overload >> operator
ostream & amp; operator<<(ostream & amp;out, Complex & amp;c){
    out << c.m_real <<" + "<< c.m_imag <<"i";;
    return out;
}

int main(){
    Complex a(10.0, 20.0); // Pass 2 actual parameters to the constructor, do not use the default parameters
    Complex b(89.5); // Pass 1 actual parameter to the constructor and use 1 default parameter
    Complex c; // Do not pass actual parameters to the constructor, use all default parameters
    a = 25.5; // Call the conversion constructor (pass 1 actual parameter to the constructor, use 1 default parameter)

    return 0;
}

The simplified constructor contains two default parameters. When calling it, you can omit some or all of the actual parameters, that is, you can pass 0, 1, or 2 actual parameters to it. The conversion constructor is a constructor that contains one parameter, which happens to be “fused” with the other two ordinary constructors.

Type conversion function:

A conversion constructor can convert other types to the current class type (for example, double to Complex), but not the other way around (for example, Complex to double).
C++ provides Type conversion function to solve this problem. The function of the type conversion function is to convert the current class type to other types. It can only appear in the form of member functions, that is, it can only appear in the class.

The syntax format of the type conversion function is:

operator target type type() {
return target type data data;
}
operator is a C++ keyword, type is the target type to be converted, and data is the data of type type to be returned.

Because the target type to be converted is type, the return value data must also be of type type. Now that we already know that type type data is to be returned, there is no need to explicitly give the return value type like a normal function. The result of this is: The type conversion function seems to have no return value type, but in fact the return value type is implicitly specified.

The type conversion function also has no parameters, because the object of the current class is to be converted to other types, so the parameters are self-evident. In fact, the compiler will assign the address of the current object to the this pointer, so that the current object can be manipulated within the function body.

#include <iostream>
using namespace std;

//plural class
class Complex{
public:
    Complex(): m_real(0.0), m_imag(0.0){ }
    Complex(double real, double imag): m_real(real), m_imag(imag){ }
public:
    friend ostream & amp; operator<<(ostream & amp;out, Complex & amp;c);
    friend Complex operator + (const Complex & amp;c1, const Complex & amp;c2);
    operator double() const { return m_real; } //type conversion function
private:
    double m_real; //real part
    double m_imag; //imaginary part
};

//Overload >> operator
ostream & amp; operator<<(ostream & amp;out, Complex & amp;c){
    out << c.m_real <<" + "<< c.m_imag <<"i";;
    return out;
}

//Overload the + operator
Complex operator + (const Complex & amp;c1, const Complex & amp;c2){
    Complex c;
    c.m_real = c1.m_real + c2.m_real;
    c.m_imag = c1.m_imag + c2.m_imag;
    return c;
}

int main(){
    Complex c1(24.6, 100);
    double f = c1; // Equivalent to double f = Complex::operator double( & amp;c1);
    cout<<"f = "<<f<<endl;
 
    f = 12.5 + c1 + 6; // Equivalent to f = 12.5 + Complex::operator double( & amp;c1) + 6;
    cout<<"f = "<<f<<endl;
 
    int n = Complex(43.2, 9.3); // Convert to double first, then to int
    cout<<"n = "<<n<<endl;

    return 0;
}

operation result:
f=24.6
f=43.1
n=43

In this example, the type conversion function is very simple, just returning the value of the member variable m_real, so it is recommended to write it in inline form.

Type conversion functions are very similar to operator overloading. They both use the operator keyword, so type conversion functions are also called type conversion operators.

Special instructions for type conversion functions:

  1. The target type type can be a built-in type, a class type, or a type alias defined by typedef. Any type that can be used as a function return type (except void) can be supported. Generally speaking, conversion to array or function types is not allowed, conversion to pointer types or reference types is OK.
  2. Type conversion functions can only be defined as member functions of a class and cannot be defined as friend functions or ordinary functions of the class because the subject of the conversion is the object of this class.
  3. Type conversion functionsgenerally do not change the object being converted, so they are usually defined as const members.
  4. Type conversion functions can be inherited and can be virtual functions.
  5. Although a classcan have multiple type conversion functions (similar to function overloading), if the target types to be converted by multiple type conversion functions themselves can be converted to each other (similar types), then sometimes two types of conversions will occur. Meaning. Take the Complex class as an example, assuming it has two type conversion functions:
operator double() const { return m_real; } // Convert to double type
operator int() const { return (int)m_real; } // Convert to int type

Then the following writing will cause ambiguity:

Complex c1(24.6, 100);
float f = 12.5 + c1;

The compiler can call operator double() to convert c1 to double type, or it can call operator int() to convert c1 to int type. Both types can be added to 12.5, and converting from Complex to double is the same as converting from Complex Converting to int is equal, no one has a higher priority, so at this time the compiler doesn’t know which function to call, and simply throws an ambiguity error for the user to solve.

  • Unable to suppress implicit type conversion function calls;
  • Type conversion functions may conflict with conversion constructors (ambiguity).

explicit keyword:
First of all, the explicit keyword in C++ can only be used to modify a class constructor with only one parameter (conversion constructor). Its function is to indicate that the constructor is explicit rather than implicit. Another corresponding keyword is implicit, which means implicit. Class constructors are declared as implicit by default. So what is the difference between an explicitly declared constructor and an implicitly declared one? Consider the following example:

class CxString {
   public:
    char *_pstr;
    int _size;

    CxString(int size) { // No explicit keyword is used, that is, the default is implicit declaration
        _size = size; // The default size of string
        _pstr = (char*)malloc(size + 1); // Allocate memory for string
        memset(_pstr, 0, size + 1);
    }

    CxString(const char *p) {
        int size = strlen(p);
        _pstr = (char*)malloc(size + 1); // Allocate memory for string
        strcpy(_pstr, p); // Copy string
        _size = strlen(_pstr);
    }

    // The destructor will not be discussed here, omitted...
};

//The following is the call:

CxString string1(24); // This is OK, pre-allocate 24 bytes of memory for CxString
CxString string2 = 10; // This is OK, pre-allocate 10 bytes of memory for CxString
CxString string3; // This does not work because there is no default constructor, the error is: "CxString": No suitable default constructor is available

CxString string4("aaaa"); // This is OK
CxString string5 = "bbb"; // This is OK, the call is CxString(const char *p)
CxString string6 = 'c'; // This is OK. In fact, CxString(int size) is called, and size is equal to the ascii code of 'c'

string1 = 2; // This is OK, pre-allocate 2 bytes of memory for CxString
string2 = 3; // This is OK, pre-allocate 3 bytes of memory for CxString

In the above code, CxString string2 = 10; Why is this sentence OK? Because C++ uses a constructor with only one parameter as a conversion constructor — converting the corresponding data type to the class type.

However, _size in the above code represents the size of the string memory allocation, so the second sentence of the call CxString string2 = 10; and the sixth sentence CxString string6 = ‘c’; are ambiguous. The compiler will convert ‘char’ into int, which is _size, which is not the result we want. Is there any way to prevent this usage? The answer is to use the explicit keyword. Modify the above code as follows:

class CxString {
   public:
    char *_pstr;
    int _size;

    explicit CxString(int size) { // Use keyword explicit statement to force explicit conversion
        _size = size;
        //The code is the same as above, omitting...
    }
    CxString(const char *p) {
        //The code is the same as above, omitting...
    }
};

//The following is the call:
CxString string1(24); // This is OK
CxString string2 = 10; // This does not work because the explicit keyword cancels the implicit conversion
CxString string3; // This doesn't work because there is no default constructor
CxString string4("aaaa"); // This is OK
CxString string5 = "bbb"; // This is OK, the call is CxString(const char *p)
CxString string6 = 'c'; // This is not possible. In fact, CxString(int size) is called, and _size is equal to the ascii code of 'c', but the explicit keyword cancels the implicit conversion

string1 = 2; // This doesn't work either, because the implicit conversion is cancelled.
string2 = 3; // This doesn't work either, because the implicit conversion is cancelled.

The function of the explicit keyword is to prevent implicit automatic conversion of the class constructor (It is forbidden to implicitly call the conversion constructor, only forced type conversion, that is, explicit conversion.), as mentioned above However, the explicit keyword is only valid for class constructors with one parameter. If the class constructor parameters are greater than or equal to two, implicit conversion will not occur, so the explicit keyword is invalid.

However, there is an exception, that is, when all parameters except the first parameter have default values, the explicit keyword is still valid. At this time, only one parameter is passed in when the constructor is called. , equivalent to a class constructor with only one parameter, the example is as follows:

class CxString {
   public:
    int _age;
    int _size;

    // Declaration using keyword explicit
    explicit CxString(int age, int size = 0) {
        _age = age;
        _size = size;
        //The code is the same as above, omitting...
    }

    CxString(const char *p) {
        //The code is the same as above, omitting...
    }
};
//The following is the call:
CxString string1(24); // This is OK
CxString string2 = 10; // This does not work because the explicit keyword cancels the implicit conversion
CxString string3; // This doesn't work because there is no default constructor

string1 = 2; // This doesn't work either, because the implicit conversion is cancelled.
string2 = 3; // This doesn't work either, because the implicit conversion is cancelled.
string3 = string1; // This will not work, because the implicit conversion is cancelled, unless the class implements the overloading of the operator "="

The explicit keyword is used to disable implicit type conversions.

C++ implicit type conversion:

The C++ language does not directly add two values of different types, but first tries to unify the types of the operands according to type conversion rules before evaluating. The above type conversion is performed automatically without the intervention of the programmer, and sometimes the programmer does not even need to understand it. ?Therefore, they are called implicit conversions. ?

In the following cases, the compiler will automatically convert the type of the operand:

  • In mostexpressions, integer values smaller than type int are firstpromoted to larger integer types.
  • In a condition, a non-Boolean value is converted to a Boolean type.
  • During the initialization process, the initial value is converted into the type of the variable.
  • In an assignment statement, the right operand isconverted to the type of the left operand. (Similar to initialization)
  • If the operands of arithmetic operations or relational operations have multiple types, they need to be converted into the same type.
  • Type conversion also occurs during function calls.

Arithmetic conversion (arithmetic conversion) means to convert one arithmetic type into another arithmetic type.

The rules of arithmetic conversion define a set of types of conversion levels, among which
The operand of the operator will be converted to the widest type

The above are the original words in “Primer C++”. I originally thought that the widest type meant that it occupied the largest number of bits in the machine, but there was another sentence after:

For example, if an operand is of type
long double, then no matter what the type of the other operand is
will be converted to long double. There is also a more general case, when there are both floating point and integer types in the expression, the integer value will
Convert to the corresponding floating point type

?We know the number of bits occupied by the various arithmetic types in the machine:

32-bit 64-bit Whether it changes
bool 1
char 1 1 No change
* pointer 4 8 Change
short int 2 2 No change
int 4 4 No change
unsigned int 4 4 No change
float 4 4 No change
double 8 8 No change
long 4 8 Change
unsigned long 4 8 Change
long long 8 8 No change
string 32
void 1 1 No change

Except that * pointers and long change as the operating system word length changes. Everything else is fixed (32-bit vs. 64-bit).

If width is interpreted as a number of bits, obviously when there are both floating point types and integer types in the expression, they should not all be converted to floating point types (for example, long long occupies more bits than float more, no need to convert to floating point number). Therefore this explanation is wrong.

So what exactly does this width mean?

Integer improvement:

Integral promotion is responsible for converting small integer types into larger integer types (similarly, the meaning of small and large is also questionable here, which will be summarized in the end) . Highest precedence in arithmetic conversions.

For types such as bool, char, signed char, unsigned char, short and unsigned short, as long as all possible values can exist in int, they will be promoted to int type< /strong>. Otherwise, promote to type unsigned int . as we know

  • The Boolean value false is promoted to 0 and true is promoted to 1.
  • 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 thatthe converted type can accommodate the original All possible values of type.

Although int is called a “big type”, this can be explained by the number of bits of the data type, and no useful information can be obtained.

In addition, I think the premise that “the converted type must be able to accommodate all possible values of the original type” can explain to a certain extent C++’s thinking on type conversion, that is, > Lose as little information as possible of the original type.

Unsigned type operand

If the operand types of an operator are inconsistent, the operands will be converted to the same type . But if the type of an operand is an unsigned type, then the conversion result will depend on the relative size of each integer type in the machine.

As usual, perform the integer promotion first. If the result types match, no further conversion is necessary. If the types of both (promoted) operands are eithersigned or both unsigned, thenthe operand of the smaller type is converted to the larger type (similarly Don’t know the meaning of small and big).

Ifone operand is an unsigned type and the other operand is a signed type, and the unsigned type is not smaller than the signed type, thenthe signed type The operand is converted to unsigned. For example, assuming that the two types are unsigned int and int, the operand of type int is converted to type unsigned int. It should be noted that if the value of type inthappens to be a negative value, the result will be converted by the method introduced in “Section 2.1.2 (page 32)” and bring the description in this section All “side effects”.

If there are both signed and unsigned types in the expression, it will occur when the signed type is negative.
Unusual result because
Signed numbers are automatically converted to unsigned numbers. For example, in an expression of the form a*b, if a=-1, b=1, and both a and b are ints, the value of the expression is obviously -1. However, if a is an int and b is unsigned, the result must be
Depends on the number of digits occupied by int on the current machine. In our environment, the result is 4294967295.

The remaining case is when thesigned type is larger than the unsigned type, and the result of the conversion is machine-dependent. An operand of an unsigned type is converted to a signed type if all values of the unsigned type can exist in the signed type. If not, then the operand of signed type is converted to unsigned type.

For example, if the types of the two operands are long and unsigned int respectively, and the size of int and long are the same, the long type operand is converted to the unsigned int type: If long type If it occupies more space than int, the operand of unsigned int type is converted to long type.

(You can see that in the hypothesis, the relative sizes of int and long change, so the size of the type is even less likely to refer to the number of bits. The term “occupied space” is used here, and we all Knowing that the data typeoccupies a fixed space in memory, then the space occupied here should have another explanation.) <-This is my wrong idea< /strong>, because I later remembered that the number of bits of long will change in 32-bit and 64-bit machines.

Conclusion:

After thinking about it, I feel that the size of the data type should indeed refer to the space occupied in the memory.

AndThe width of the data type. In the example given, there is only the information that all integers will be converted into floating point types, and the idea of losing as little information as possible of the original types. In this case, there is only one truth in the end (glasses reflection). This width refers to the width of the data on the screen, and the integer and decimal parts are calculated separately (I guess).

“When there are both floating point types and integer types in the expression, the integer value will beconverted into the corresponding floating point type” The width of the decimal part of the integer type is 0, which is smaller than the floating point number, so it needs to Convert to floating point number.

“Large 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 thatthe converted type can accommodate “All possible values of the original type” can also be explained. As long as the width of the integer part is larger, it must be able to accommodate all values.

It stands to reason that relying on reasoning (guessing) is not enough, but I really can’t find anyone on the search engine who has explained this width. It is not enough to understand it from a lower level perspective. I may fill in the holes when I learn more in the future. , for the time being, it is just for the convenience of my own understanding.

Implicit type conversion diagram:

syntaxbug.com © 2021 All Rights Reserved.