CMake: Set Language Standard (Part One)

CMake: Set Language Standard (Part One)CMake: Set Language Standard (Part One)

Introduction:

Programming languages have different standards, and enabling a new standard is achieved by setting appropriate compiler flags. First, let’s understand the relevant features of C++ 11. For information on setting language features, please refer to the previous or earlier articles regarding CMakeLists.txt, which also cover this topic!

CMake: Set Language Standard (Part One)

C++ Standard History

  • In 1998, the C++ standards committee released the first version of the C++ standard, named the C++ 98 standard.

  • In 2011, the new C++ 11 standard was born to replace the C++ 98 standard, and it is also known as C++ 0x.

  • In 2014, the C++ 14 standard was released, which made better modifications and updates to the C++ 11 standard library.

  • At the end of 2017, the C++ 17 standard was officially promulgated.

Introduction to C++ 11 Features

Before the C++ 11 standard, the C++ standards committee also modified the C++ 98 standard in 2003 (known as the C++ 03 standard), but since it only fixed some bugs in the C++ 98 standard without modifying the core syntax, it is commonly referred to as the C++98/03 standard.

Among the three standards above, the C++ 11 standard is undoubtedly revolutionary, correcting about 600 defects in the C++ language and adding about 140 new features, making the C++ language feel fresh.

01

Type Deduction with auto and decltype

In versions of C++11 prior to this, when defining or declaring a variable, its type had to be specified, such as int, char, etc. The C++11 standard uses the auto keyword to support automatic type deduction.

In previous versions of C++, auto was used to specify the storage type of a variable, which was relative to static. auto indicated that the variable was automatically stored, which is also the default rule of the compiler, so it made no difference whether it was written or not, making the existence of auto quite redundant.

The C++ 11 standard gives auto a new meaning, using it for automatic type deduction. That is, after using the auto keyword, the compiler will automatically deduce the type of the variable during compilation.

Note:

  • auto is merely a placeholder, and during compilation, it will be replaced by the actual type. Variables in C++ must have a defined type, but this type is deduced by the compiler itself.

  • Variables using auto type deduction must be initialized immediately, as auto in C++11 is just a placeholder, not a true type declaration like int.

Combining auto with const

int x = 0;
// n is const int, auto is deduced as int
const auto n = x;
// f is const int, auto is deduced as int (const attribute is discarded)
auto f = n;
// r1 is const int & type, auto is deduced as int
const auto &r1 = x;
// r1 is const int& type, auto is deduced as const int type
auto &r2 = r1;

auto combined with const usage:

  • When the type is not a reference, the deduction result of auto will not retain the const attribute of the expression;

  • When the type is a reference, the deduction result of auto will retain the const attribute of the expression. auto restrictions:

  • When using auto, the variable must be initialized.

  • auto cannot be used as a function parameter.

  • auto cannot act on non-static member variables of a class.

  • auto cannot define arrays.

  • auto cannot act on template parameters.

decltype is a new keyword added in C++11, which functions similarly to auto, used for automatic type deduction at compile time. decltype is short for declare type.

auto is not suitable for all automatic type deduction scenarios, and in some special cases, using auto is very inconvenient or even impossible, so the decltype keyword is also introduced in C++11.

auto var_name = value;
decltype(exp) var_name = value;

Here, var_name represents the variable name, value represents the value assigned to the variable, and exp represents an expression.

auto deduces the variable type based on the initial value value on the right side of =, while decltype deduces the variable type based on the exp expression, which is independent of value on the right side of =.

auto requires the variable to be initialized, while decltype does not.

exp is a regular expression that can take any complex form, but it must ensure that the result of exp has a type and cannot be void; for example, when exp calls a function that returns void, the result of exp will also be of type void, which will lead to a compilation error.

int a = 0;
// b is deduced as int
decltype(a) b = 1;  
// x is deduced as double
decltype(10.8) x = 5.5;  
// y is deduced as double
decltype(x + 100) y;  

decltype Deduction Rules

  • If exp is an expression not surrounded by parentheses (( )), or a member access expression, or a standalone variable, then decltype(exp) has the same type as exp.

  • If exp is a function call, then decltype(exp) has the same type as the function’s return value type.

  • If exp is an lvalue or surrounded by parentheses (( )), then decltype(exp) is of the type exp‘s reference; assuming exp‘s type is T, then decltype(exp)‘s type is T&.

Note: An lvalue refers to data that still exists after the expression execution ends, i.e., persistent data; an rvalue refers to data that does not exist after the expression execution ends, i.e., temporary data. A simple way to distinguish lvalues and rvalues is to take the address of the expression. If the compiler does not report an error, it is an lvalue; otherwise, it is an rvalue.

auto and decltype handling of cv qualifierscv qualifiers refer to the const and volatile keywords:

  • The const keyword indicates that the data is read-only and cannot be modified.

  • volatile is the opposite of const; it indicates that the data is mutable and volatile,

    with the purpose of preventing the CPU from caching the data in registers, instead reading from the original memory.

When deducing variable types, auto and decltype handle cv qualifiers differently. decltype retains cv qualifiers, while auto may discard cv qualifiers. The principle can be seen in Combining auto with const.

auto and decltype handling of references When the expression type is a reference, the deduction rules for auto and decltype are also different; decltype retains the reference type, while auto discards the reference type and directly deduces its original type.

02

C++ Return Value Type Trailing

In generic programming, if the return type needs to be obtained through the operation of parameters:

template <typename R, typename T, typename U>
R Add(T t, U u)
{
    return t+u;
}

int main() {
  int a = 1;
  float b = 2.0f;
  auto c = Add<decltype(a + b)>(a + b);
  return 0;
}

The above code is because we do not care what the type of a + b is, so we just need to get the return value type directly through decltype(a + b).

The above usage process is very inconvenient because the outside does not actually know how the parameters should operate; only the Add function knows how the return value should be deduced.

Directly obtaining the return value through decltype in the function definition:

template <typename T, typename U>
decay_t(T() + U()) add(T t, U u) {
    return t + u;
}

Considering that T and U may be classes without a default constructor, the correct writing is as follows:

template <typename T, typename U>
decay_t((*(T*)0) + (*(U*)0)) add(T t, U u) {
    return t + u;
}

The above code successfully uses decltype to complete return value deduction, but the writing is too obscure, greatly increasing the difficulty of using decltype in return value type deduction and reducing code readability.

Therefore, in C++11, a return type trailing syntax was added to complete the return value type deduction by combining decltype and auto.

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u){
    return t + u;
}

03

Improvements to Consecutive Right Angle Brackets in Template Instantiation

In C++98/03 generic programming, during template instantiation, two consecutive right angle brackets (>>) would be interpreted by the compiler as the right shift operator rather than the end of the template parameter list.

template <typename T>
struct Foo{
  typedef T type;
};

template <typename T>
class A{
  // ...
};

int main(){
  // Compilation error
  Foo<A<int>>::type xx;  
  return 0;
}

The above code, when compiled with gcc, would yield the following error message:

error: ‘>>’ should be ‘>>’ within a nested template argument list Foo<A>::type xx;

This means that the syntax Foo<A<int>> is not supported and must be written as Foo<A<int> > (note the space between the two right angle brackets).

This limitation is unnecessary. Among all paired parentheses in C++, only two consecutive right angle brackets exhibit this ambiguity. static_cast, reinterpret_cast, and other C++ standard conversion operators use <> to obtain the type to convert to (type-id). If this type-id itself is a template, it becomes very inconvenient to use.

In the C++11 standard, compilers are required to treat right angle brackets in templates separately, enabling them to correctly determine whether >> is a right shift operator or the end marker of a template parameter list.

Note: The above automation may sometimes be incompatible with older standards:

template &lt;int N&gt;
struct Foo{
  // ...
};

int main() {
  // Solution:
  // Foo&lt;(100 &gt;&gt; 2)&gt; xx;
  Foo&lt;100 &gt;&gt; 2&gt; xx; 
  return 0;
}

Compiling with a C++98/03 compiler is fine, but with a C++11 compiler, it will display:

error: expected unqualified-id before ‘&gt;’ token Foo&lt;100 &gt;&gt; 2&gt; xx;

Using using to Define Aliases (Replacing typedef)

C++ can use typedef to redefine a type, and the redefined type may not necessarily be a new type; it could also simply be an existing type with a new name. Using typedef to redefine types is very convenient, but it also has some limitations, such as not being able to redefine a template.

template&lt;typename T&gt;
using str_map_t = std::map&lt;std::string, T&gt;;
// ...
str_map_t&lt;int&gt;map_1;

In fact, the alias syntax of using covers all the functionalities of typedef.

// Redefine unsigned int
typedef unsigned int uint_t;
using uint_t = unsigned int;
// Redefine std::map
typedef std::map&lt;std::string, int&gt; map_int_t;
using map_int_t = std::map&lt;std::string, int&gt;;

// Redefine templates
// C++98/03 
template &lt;typename T&gt;
struct func_t{
    typedef void (*type)(T, T);
};
// Using func_t template
func_t&lt;int&gt;::type xx_1;
// C++11 
template &lt;typename T&gt;
using func_t = void (*)(T, T);
// Using func_t template
func_t&lt;int&gt; xx_2;

From the examples, it can be seen that the syntax for defining template aliases using using simply adds the template parameter list to the ordinary type alias syntax. Using using makes it easy to create a new template alias without the cumbersome external template syntax required in C++98/03.

04

Support for Default Parameters in Function Templates

In C++98/03, class templates could have default template parameters:

template &lt;typename T, typename U = int, U N = 0&gt;
struct Foo{
    // ...
};

However, function default template parameters were not supported:

// error in C++98/03: default template arguments
template &lt;typename T = int&gt;  
void func(){
    // ...
}

This limitation has been lifted in C++11. The above func function can be used directly in C++11:

int main(void){
	//T = int
    func();   
    return 0;
}

Default template parameters for function templates have some differences in usage rules compared to other default parameters; they do not have to be placed at the end of the parameter list. Additionally, based on the actual situation in which the function template is called, the compiler can also deduce the types of some template parameters on its own. That is, when default template parameters are combined with the compiler’s ability to deduce template parameter types, the code writing becomes exceptionally flexible. We can specify that some of the template parameters in the function use default parameters while others use automatic deduction:

template &lt;typename R = int, typename U&gt;
R func(U val){
    return val;
}

int main(){
	// R=int, U=int
    func(97);               
	// R=char, U=int
    func&lt;char&gt;(97);         
	// R=double, U=int
    func&lt;double, int&gt;(97);  
    return 0;
}

When default template parameters and the compiler’s deduction of template parameter types are used together, if the compiler cannot deduce the types of function template parameters, it will choose to use the default template parameters; if the template parameters cannot be deduced and no default values are set, the compiler will directly report an error.

template &lt;typename T, typename U = double&gt;
void func(T val1 = 0, U val2 = 0) {
    //...
}

int main() {
	// T=char, U=double
    func('c'); 
	// Compilation error
    func();    
    return 0;
}

05

Using Variadic Parameters in Function Templates and Class Templates

Variadic parameters refer to parameters whose number and types can be arbitrary.

For function parameters, C++ has always supported setting variadic parameters for functions, the most typical representative being the printf() function, which has the syntax format:

int printf ( const char * format, ... );

... indicates variadic parameters, meaning that the printf() function can accept any number of parameters, and the types of each parameter can be different.

printf("%d", 10);
printf("%d %c",10, 'A');
printf("%d %c %f",10, 'A', 1.23);

Usually, the variadic parameters that can accommodate multiple parameters are called parameter packs. With the help of the format string, the printf() function can easily determine the number and types of parameters in the parameter pack.

#include &lt;iostream&gt;
#include &lt;cstdarg&gt;

// Function with variadic parameters
void vair_fun(int count, ...) {    
	va_list args;    
	va_start(args, count);    
	for (int i = 0; i &lt; count; ++i) {        
		int arg = va_arg(args, int);        
		std::cout &lt;&lt; arg &lt;&lt; " ";    
	}    
	va_end(args);
}

int main() {    
	// There are 4 variadic parameters: 10, 20, 30, 40    
	vair_fun(4, 10, 20, 30,40);    
	return 0;
}

To use the parameters in the parameter pack, you need to use the three macros with parameters from the <cstdarg> header: va_start, va_arg, and va_end:

  • va_start(args, count): args is a variable of type va_list, which can simply be viewed as char * type. With the help of the count parameter, it finds the starting position of the variadic parameters and assigns it to args;

  • va_arg(args, int): After va_start finds the starting position of the variadic parameters, specifying the parameter type as int, va_arg can return the first parameter in the variadic parameters;

  • va_end(args): After the args variable is no longer used, the va_end macro should be called promptly to clean up the args variable.

When using ... variadic parameters, the following points should be noted:

  • ... variadic parameters must be the last parameter of the function, and a function can have at most one variadic parameter;

  • There must be at least one named parameter before the variadic parameters;

  • When the variadic parameters include char type parameters, the va_arg macro should read them as int type; when the variadic parameters include short type parameters, the va_arg macro should read them as double type.

It should be noted that, the ... variadic parameter method only applies to function parameters and does not apply to template parameters.Experimental Score

06

Variadic Template

Before the C++ 11 standard was released, function templates and class templates could only set a fixed number of template parameters. The C++11 standard extended the functionality of templates, allowing templates to contain any number of template parameters, known as variadic templates.

Variadic Function Template

template&lt;typename... T&gt;
void vair_fun(T...args) {
    // Function body
}

In the template parameters, typename (or class) followed by ... indicates that T is a variadic template parameter that can accept multiple data types, also known as a template parameter pack. In the vair_fun() function, the parameter args is of type T..., indicating that the args parameter can accept any number of parameters, also known as a function parameter pack. Thus, the instantiated vair_fun() function can specify any types and any number of parameters.

vair_fun();
vair_fun(1, "abc");
vair_fun(1, "abc", 1.23);

To

Leave a Comment