Automatic type derivation

Keywords: C++

[From]
Author: Su Fang
Links: https://subingwen.cn/cpp/autotype/
Source: Big C who likes programming

Many new features have been added to C++11, such as the ability to use auto to automatically derive the type of variable and the ability to combine decltype to represent the return value of a function. Using new features allows us to write simpler, more modern code.

1. auto

In C++98, auto and static correspond, indicating that variables are automatically stored, but non-static local variables are automatically stored by default, so this keyword has become very cocktail. In C++11, they give new meaning, using this keyword can automatically deduce the actual type of variables like other languages.

1.1 Derivation Rule

In C++11, auto does not represent an actual data type, it is just a placeholder for type declaration. Auto does not always deduce the actual type of a variable in any scenario. Variables declared with auto must be initialized so that the compiler can deduce its actual type and replace the auto placeholder with the real type at compile time. The usage syntax is as follows:

auto Variable Name = Variable Value;

Based on the above grammar, here are some simple examples:

auto x = 3.14;      // x is floating point
auto y = 520;       // y is a reshaping int
auto z = 'a';       // z is a character char
auto nb;            // error, variable must be initialized
auto double nbl;    // Syntax error, cannot modify data type

Limitations of 1.2 auto

The auto keyword is not omnipotent, and type inference cannot be accomplished in these scenarios:

Limit 1: Cannot be used as a function parameter. Since arguments are only passed to function parameters when a function is called, auto requires that the modifier variable be assigned a value, so the two contradict.

int func(auto a, auto b)   // ERROR: a parameter cannot have a type that contains 'auto'
{
    std::cout << "a: " << a <<", b: " << b << std::endl;
}

Limit 2: Cannot be used for initialization of non-static member variables of a class

class Test
{
    auto v1 = 0;                    // ERROR: 'v1': a non-static data member cannot have a type that contains 'auto'
    static auto v2 = 0;             // ERROR: 'Test::v2': a static data member with an in - class initializer must have non - volatile const integral type or be specified as 'inline'
    static const auto v3 = 10;      // ok
}

Limit 3: Arrays cannot be defined using the auto keyword

int func()
{
    int array[] = {1,2,3,4,5};  // Define Array
    auto t1 = array;            // ok, t1 is deduced as int*type
    auto t2[] = array;          // error, auto cannot define array
    auto t3[] = {1,2,3,4,5};    // error, auto cannot define array
}

Limit 4: Template parameters cannot be derived using auto

template <typename T>
struct Test{}

int func()
{
    Test<double> t;
    Test<auto> t1 = t;           // error, cannot deduce template type
    return 0;
}

1.3 Application of Auto

After defining an stl container before C++11, this code is often written when traversing:

#include <map>
int main()
{
    std::map<int, std::string> person;
    std::map<int, std::string>::iterator it = person.begin();
    for (; it != person.end(); ++it)
    {
        // do something
    }
    return 0;
}

You can see that the code is very long when defining the iterator variable it, which is cumbersome to write and refreshes a lot after using auto:

#include <map>
int main()
{
    std::map<int, std::string> person;
    // Code Simplification 1
    for (auto it = person.begin(); it != person.end(); ++it) {
        // do something
    }

    // Code Simplification 2
    for (auto it : person) {
        // do something
    }

    return 0;
}

2. decltype

In some cases, you don't need or can't define variables, but you want to get a type. Then you can use the decltype keyword provided by C++11, which is used to derive the type of an expression when the compiler compiles. The syntax format is as follows:

decltype (Expression)

Decltype is short for declare type, meaning declare type. The decltype derivation is done at compile time. It is only for the derivation of expression types and does not calculate the value of expressions. Let's look at a simple set of examples:

int a = 10;
decltype(a) b = 99;                 // b -> int
decltype(a+3.14) c = 52.13;         // c -> double
decltype(a+b*c) d = 520.1314;       // d -> double

You can see that the decltype deduced expression can be simple and complex. At this point auto cannot do it. Auto can only deduce the initialized variable type.

2.1 Derivation Rules

Scenario 1: The expression is a common variable or expression or a class expression, in which case the type derived using decltype is identical to the type of expression.

#include <iostream>
#include <string>

class Test
{
public:
    std::string text;
    static const int value = 110;
};

int main()
{
    int x = 99;
    const int &y = x;
    decltype(x) a = x;
    decltype(y) b = x;
    decltype(Test::value) c = 0;

    Test t;
    decltype(t.text) d = "hello, world";
    return 0;
}
  • Variable a is deduced to be of type int
  • Variable b is derived as const int &type
  • Variable c is deduced to be of type const int
  • Variable d is derived as std::string type

Scenario 2: The expression is a function call, and the type inferred using decltype matches the return value of the function.

class Test{...};
//Function declaration
int func_int();                 // Return value is int
int& func_int_r();              // The return value is int&
int&& func_int_rr();            // The return value is int &&
const int func_cint();          // Return value is const int
const int& func_cint_r();       // The return value is const int&
const int&& func_cint_rr();     // The return value is const int &&
const Test func_ctest();        // Return value is const Test

//decltype type derivation
int n = 100;
decltype(func_int()) a = 0;
decltype(func_int_r()) b = n;
decltype(func_int_rr()) c = 0;
decltype(func_cint())  d = 0;
decltype(func_cint_r())  e = n;
decltype(func_cint_rr()) f = 0;
decltype(func_ctest()) g = Test();
  • Variable a is deduced to be of type int
  • Variable b is deduced as int&type
  • Variable c is deduced as int&type
  • Variable d is deduced to be of type int
  • Variable e is derived as const int &type
  • Variable f is derived as const int&type
  • Variable g is derived as const Test type

Function func_cint () returns a pure right value (data that no longer exists after the expression is executed, that is, temporary data). For pure right values, only class types can carry const and volatile qualifiers. In addition, these two qualifiers need to be ignored, so the derived variable d is of type int instead of const int.

Scenario 3: An expression is a left value, or surrounded by parentheses (), and a reference to the expression type is derived using decltype (const, volatile qualifiers cannot be ignored if any).

#include <iostream>
#include <vector>

class Test
{
public:
    int num;
};

int main() {
    const Test obj;
    //Parenthesized expression
    decltype(obj.num) a = 0;
    decltype((obj.num)) b = a;
    //additive expression
    int n = 0, m = 0;
    decltype(n + m) c = 0;
    decltype(n = n + m) d = n;
    return 0;
}
  • obj.num is a member access expression for the class, which conforms to Scenario 1, so a is of type int
  • obj.num is parenthesized and conforms to scene 3, so b is of type const int&
  • n+m gets a right value that matches Scene 1, so c is of type int
  • n=n+m gets a left value n, which corresponds to scene 3, so d is of type int&

2.2 Application of decltype

Applications of decltype are often found in generic programming. For example, we write a class template that adds a function to traverse containers, as follows:

#include <list>

template <class T>
class Container
{
public:
    void func(T& c)
    {
        for (m_it = c.begin(); m_it != c.end(); ++m_it)
        {
            std::cout << *m_it << " ";
        }
        std::cout << std::endl;
    }
private:
    ??? m_it;  // The iterator type cannot be determined here
};

int main()
{
    const std::list<int> lst;
    Container<const std::list<int>> obj;
    obj.func(lst);
    return 0;
}

M_in program It has a problem here. There are two types of iterator variables: read-only (T:: const_iterator) and read-write (T::iterator). With decltype, you can solve this problem perfectly. When T is a non-const container, you get a T::iterator, and when T is a const container, you get a T::const_iterator. Iterator.

#include <list>
#include <iostream>

template <class T>
class Container
{
public:
    void func(T& c)
    {
        for (m_it = c.begin(); m_it != c.end(); ++m_it)
        {
            std::cout << *m_it << " ";
        }
        std::cout << std::endl;
    }
private:
    decltype(T().begin()) m_it;  // Successfully deduce T type using decltype
};

int main()
{
    const std::list<int> lst{ 1,2,3,4,5,6,7,8,9 };
    Container<const std::list<int>> obj;
    obj.func(lst);
    return 0;
}

3. Postposition of return type

In generic programming, you may need to calculate parameters to get the type of return value, such as the following scenario:

#include <iostream>
// R->Return Value Type, T->Parameter 1 Type, U->Parameter 2 Type
template <typename R, typename T, typename U>
R add(T t, U u)
{
    return t + u;
}

int main()
{
    int x = 520;
    double y = 13.14;
    // auto z = add<decltype(x + y), int, double>(x, y);
    auto z = add<decltype(x + y)>(x, y);  // Simplified Writing
    std::cout << "z: " << z << std::endl;
    return 0;
}

As for the return value, it can be inferred from the code above that the result type of the expression t+u is the same, so it can be inferred from decltype, and the parameters T and U of the template function can be automatically inferred from the actual parameters, so they can also be omitted from the program. Although the above approach solves the problem, the solution is a bit too idealized because the caller does not know what kind of processing is performed inside the function.

So if you want to solve this problem, you have to write directly on the add function. First, let's look at the first way:

template <typename T, typename U> //ERROR: error C2065: 't': undeclared identifier
decltype(t+u) add(T t, U u)
{
    return t + u;
}

When we change these lines of code in the compiler, we get a direct error, so t and u in the decltype are both function parameters, which directly corresponds to variables being used without defining them.

In C++11, a post-type syntax for return types is added, which means that decltype and auto are combined to complete the derivation of return types. Its grammatical format is as follows:

// Symbol - > is followed by the type of function return value
auto func(Parameter 1, Parameter 2, ...) -> decltype(Parameter expression)

By analyzing the syntax code behind the return type mentioned above, it is concluded that auto tracks the type decltype() derives, so the add() function above can be modified as follows:

#include <iostream>

template <typename T, typename U>
// Return type postposition syntax
auto add(T t, U u) -> decltype(t+u) //OK
{
    return t + u;
}

int main()
{
    int x = 520;
    double y = 13.14;
    // auto z = add<int, double>(x, y);
    auto z = add(x, y);	// Simplified Writing
    std::cout << "z: " << z << std::endl;
    return 0;
}

Posted by atwyman on Tue, 30 Nov 2021 11:53:54 -0800