decltype and template function specialization in type extraction

Table of Contents

foreword

1. The decltype keyword

1. Get variable type

2. Get the expression type

3. Get the expression (function) type

4. Used for the return value type of the perfect forwarding function in the proxy mode

2. Partial specialization and full specialization of templates

3. Functions can also be used as function parameters

Summarize

Foreword

This article will explain from various aspects the usefulness of the decltype keyword proposed in C++11 and the compiler’s selection method for function calls when template functions have both specialized and partially specialized versions, which can help readers better understand templates Function calls and the magic of automatic type deduction in template types. Additionally, we’ll explore the use of functions as function arguments.

1. decltype keyword

When we need to get the type of an expression, we can use the decltype keyword to infer the type of the expression. The decltype keyword was introduced in C++11, which can obtain the type of an expression at compile time, including variables, function calls, expressions, etc.

Note that there are two key points in the role of the decltype keyword above:

  1. Get the type of the expression at compile time
  2. Get the type of the expression at compile time

Because there was a meme on the Internet before: “Please use template metaprogramming to achieve compile-time heap sorting”

The decltype keyword is used here to determine the type of the expression at compile time.

Previous Summary:
Type extraction type_traits provides metaprogramming tools for C++, such as type traits, compile-time rational arithmetic, and compile-time integer sequences.

Here you need to know the following template tools (in C++14, using aliases to simplify the use of template classes):

  • remove_const_t removes the const modifier in the type
  • remove_reference_t removes the reference modifier in the type
  • decay_t decay type (remove const and reference at the same time)
  • is_same_v Determine whether T1 and T2 are of the same type

Next, look at the usage of the decltype keyword:

decltype returns the static type of the expression, including modifiers

1. Get variable type

decltype(exp) where exp can be an expression not surrounded by (), or a class member access expression, or a single variable, then the deduced type is consistent with exp.

void test1()
{
// decltype(expression)
// Pay attention to the difference between expressions and variables. You can add parentheses to variables to force compilation into expressions
//1. Get the type of the variable:
constexpr int num1 = 10;
decltype(num1) num2 = 92; // decltype(variable) is deduced to determine the type of num1 variable as the standard
cout << is_same_v<const int, decltype(num2)> << endl; // 1 infers that num2 type is const int
cout << is_same_v<int, decltype(num2)> << endl; // 0
cout << is_same_v<int, remove_const_t<decltype(num2)>> << endl; // 1
cout << is_same_v<int, decay_t<decltype(num2)>> << endl; // 1
}

2. Get expression type

decltype(exp) If exp is an lvalue, or surrounded by (), then the deduced type is a reference to exp.

//2. Get the type of expression:
constexpr int num11 = 10;
decltype((num11)) num22 = num11; // (num22) can make the compiler think that this is an expression, not a variable
cout << is_same_v<const int, decltype(num22)> << endl; // 0
cout << is_same_v<int & amp;, decltype(num22)> << endl; // 0
cout << is_same_v<const int & amp;, decltype(num22)> << endl; // 1 infers that num22 type is const int & amp;
// ***********************************
// Note that the const modifier modified by remove_const_t acts on the object pointed to by the num22 reference. The type deduction for num22 is still const int & amp;
cout << is_same_v<const int & amp;, remove_const_t<decltype( & amp;num22)>> << endl; // 1
cout << is_same_v<int & amp;, remove_const_t<decltype(num22)>> << endl; // 0
// ***********************************
cout << is_same_v<const int, remove_reference_t<decltype(num22)>> << endl; // 1
cout << is_same_v<int, decay_t<decltype(num22)>> << endl; // 1

3. Get expression (function) type

decltype(exp) If exp is a function call, the deduced type is consistent with the return value type of the function.

First, a function that automatically deduces the return value type is given:

decltype(auto) get_id() // Automatic derivation can retain references
{
const string s = "s_t_r_i_n_g";
return s;
}

Call to receive:

//3. Get the type of function return value:
decltype(get_id()) id;

Debug window:

Don’t care that the content of the variable id is empty, because you have not initialized and assigned it, but you can see that the type of id is consistent with the type of the return value of the function, which is string, and the deduction is correct.

4. For the return value type of perfect forwarding function in proxy mode

First, a simple implementation template of the proxy mode is given:

template<typename T>
class RealObject
{
public:
RealObject(){}
RealObject(T val):m_val(val){}
void setValue(T val)
{
m_val = val;
}
T getValue()
{
return m_val;
}
private:
T m_val;
};
template<typename... T>
class ProxyObject
{
private:
RealObject<T...> realobject;
public:
ProxyObject(T... val) { realobject. setValue(forward<T...>(val)...); }
template<typename... Arg>
decltype(auto) getValue(Arg & amp;... arg) // Using decltype(auto) can facilitate the self-derivation of the return value
{
return realobject. getValue();
}
};

Give the code for instantiating and using the auto variable to automatically deduce the return value of the receiving function:

//4. In the proxy mode, it is used for the return value of the perfect forwarding function:
const vector<int> v{1, 2, 3, 4, 5};
ProxyObject proObj(v); // create proxy object
const auto val = proObj. getValue();
cout << "value = " << val << endl;

Since the vector object needs to be directly output by cout here, we need to globally overload the left shift operator corresponding to the vector container:

template<typename T>
ostream & amp; operator<<(ostream & amp; os, const vector<T> & amp; v)
{
os << '{';
for(size_t i= 0;i<v. size(); + + i)
{
os << v.at(i);
if (i != v. size() - 1) { os << ", "; }
}
os << '}';
return os;
}

operation result:

It can be seen that the proxy mode can be used to protect the proxy object, restrict access to its sensitive operations, and only provide the necessary interfaces. At the same time, the use of decltype (auto) successfully realizes code reuse to achieve multi-type function return value requirements .

2. Partial specialization and full specialization of templates

First, the template function and its partially specialized and fully specialized functions are given:

template<typename T, typename N>
void print()
{
cout << "template type" << endl;
}
template<typename T, int N = 0>
void print()
{
cout << "partial specialization" << endl;
}
template<char C = 'c', int Size = 0>
void print()
{
cout << "full specialization" << endl;
}

Then give the part of the code that calls the test:

void test2()
{
int* p = new int[10];
int size1 = _msize(p)/sizeof(p[0]);
cout << "size1 = " << size1 << endl;
//const int size2 = sizeof(p) / sizeof(p[0]); // Find the wrong size of dynamic array
//cout << "size2 = " << size2 << endl;
constexpr double b = 10.8;
print<char, decltype(b)>();
print<char, decltype(size1)>();
print<char>();
print<char, 100>();
//print<int, size1>(); The value of size1 can only be determined at runtime, so this statement compiles an error
print<>();
    print();
print<'w', 8>();
delete[] p;

}

operation result:

It can be seen that template-specified types that match fully-specialized and partially-specialized functions can also call ordinary function templates, but fully-specialized and partially-specialized functions are called first. Of course, there is a priority call rule to determine the calling choice of the function.

The priority calling rules are as follows:

  • If there is an exact matching full specialization, the full specialization will be preferred for calling.
  • If there is no fully matching full specialization, the best matching partial specialization will be selected and called.
  • If there are multiple partial specializations that also match, or there is no partial specialization, the most matching common function template will be selected for calling.

3. Functions can also be function parameters

First give the function to print the number:

void printNum(const int num)
{
cout << num << endl;
}

Then, the code and calling examples of using functions as function parameters based on template metaprogramming are given:

template<typename...T>
void do_N_case(auto func(T...), const int N)
{
for(int i = 0;i<N; + + i)
{
func(i);
}
}
void test3()
{
do_N_case<int>(printNum, 8);
}

operation result:

This module can be flexibly applied in combination with lambda expressions, and specific expansion and other knowledge points will be explained in detail in later articles.

Summary

Through the study of this blog, we have a deep understanding of the usage and application scenarios of the decltype keyword, as well as the flexibility of partial specialization and full specialization of templates. We also learned how to use functions as function parameters to achieve more flexible code design and calling methods. These knowledge and skills will help us better deal with complex type derivation and customized processing requirements in C++ programming. I hope this blog can be helpful to readers and provide some useful techniques and ideas. If you have any questions about these or need further discussion, please feel free to ask.