所有程序员都需要了解的现代C ++功能

该材料的作者(我们今天将其翻译发表)说,与几年前相比,现代形式的C ++有了很大的改善。 当然,这些变化不是立即发生的。 例如,在过去,C ++缺乏活力。 要找到一个可以说他对这种语言有温柔感情的人并不容易。 当负责标准化语言的人决定让位给创新时,一切都改变了。 在2011年,C ++成为一种动态语言,这种语言不断发展并引起程序员更多积极的情绪。

不要以为语言变得简单了。 如果不是最复杂的话,它仍然可以被称为最复杂的广泛使用的编程语言之一。 但是现代的C ++比以前更加友好了。



今天,我们将讨论该语言的一些新功能(从C ++ 11开始,顺便说一句,它已经存在8年了),这对任何程序员来说都是有用的。

自动关键字


自从auto关键字出现在C ++ 11中以来,程序员的生活变得更加轻松。 由于有了这个关键字,编译器可以在编译时输出变量类型,这使我们不必总是自己指定类型。 事实证明,这非常方便,例如,在必须使用map<string,vector<pair<int,int>>>类的数据类型的情况下。 使用auto关键字时,需要考虑一些事项。 考虑一个例子:

 auto an_int = 26; //    ,     - int auto a_bool = false; //   bool auto a_float = 26.04; //   float auto ptr = &a_float; //       auto data; // #1     ?    - . 

请注意本示例中的最后一行,其注释标记为#1 (以下,以类似的方式,我们将标记示例之后将解析的行)。 此行中没有初始化程序,您不能执行此操作。 此行上的代码使编译器无法知道相应变量的类型。

最初,C ++中的auto关键字非常有限。 然后,在该语言的最新版本中,会auto添加功能。 这是另一个示例:

 auto merge(auto a, auto b) //            auto! {   std::vector<int> c = do_something(a, b);   return c; } std::vector<int> a = { ... }; // #1 -  std::vector<int> b = { ... }; // #2  -  auto c = merge(a,b); //       

#1行和#2行使用大括号应用变量初始化-C ++ 11中的另一个新功能。

请记住,使用auto关键字时,编译器必须具有某种方式来推断变量的类型。

现在,一个有趣的问题。 如果您使用像auto a = {1, 2, 3}这样的设计会怎样? 这是什么 向量,还是导致编译错误?

实际上,C ++ 11中出现了std::initializer_list<type>形式的构造。 带括号的初始化值列表将使用auto关键字视为容器。

最后,如前所述,如果您必须处理复杂的数据结构,则编译器的类型推断可能非常有用。 这是一个例子:

 void populate(auto &data) { //    !   data.insert({"a",{1,4}});   data.insert({"b",{3,1}});   data.insert({"c",{2,3}}); } auto merge(auto data, auto upcoming_data) { //         auto result = data;   for(auto it: upcoming_data) {       result.insert(it);   }   return result; } int main() {   std::map<std::string, std::pair<int,int>> data;   populate(data);   std::map<std::string, std::pair<int,int>> upcoming_data;   upcoming_data.insert({"d",{5,3}});   auto final_data = merge(data,upcoming_data);   for(auto itr: final_data) {       auto [v1, v2] = itr.second; // #1               std::cout << itr.first << " " << v1 << " " << v2 << std:endl;   }   return 0; } 

看一下#1线。 表达式auto [v1,v2] = itr.second表示C ++ 17的新功能。这是声明变量时的所谓分解。 在该语言的早期版本中,必须分别提取每个值。 由于这种机制,执行此类操作变得更加方便。

此外,如果您需要使用链接来处理数据,只需在此构造中添加一个字符,然后将其转换为以下形式就足够了: auto &[v1,v2] = itr.second

Lambda表达式


C ++ 11引入了对lambda表达式的支持。 它们类似于JavaScript中的匿名函数,可以与没有名称的函数对象进行比较。 它们根据描述捕获各种范围的变量,为此使用了紧凑的句法结构。 此外,可以将它们分配给变量。

对于需要在代码中执行一些小操作但又不想为此编写单独函数的情况,Lambda表达式是非常有用的工具。 使用它们的另一个常见示例是创建用于比较值的函数。 例如:

 std::vector<std::pair<int,int>> data = {{1,3}, {7,6}, {12, 4}}; //        std::sort(begin(data), end(data), [](auto a, auto b) { //   - auto!   return a.second < b.second; }); 

在这个简短的示例中,您会发现很多有趣的东西。

首先,请注意使用大括号使用变量初始化有多方便。 接下来,我们可以看到标准构造begin()end() ,它们也出现在C ++ 11中。然后是lambda函数,该函数用作比较数​​据的机制。 该函数的参数使用auto关键字声明,此功能出现在C ++ 14中。以前,该关键字不能用于描述函数的参数。

现在注意,lambda表达式以方括号- []开头。 这就是所谓的变量掩码。 它确定表达式的范围,即,它允许您控制lambda表达式与局部变量和对象的关系。

这是库中专门介绍现代C ++功能的摘录:

  • [] -表达式不捕获任何内容。 这意味着在lambda表达式中,不可能使用它外部作用域中的局部变量。 表达式中只能使用参数。
  • [=] -表达式捕获局部对象的值(即局部变量,参数)。 这意味着它们可以使用,但不能修改。
  • [&] -表达式捕获对本地对象的引用。 可以修改它们,如以下示例所示。
  • [this] -表达式捕获this指针的值。
  • [a, &b] -表达式捕获对象a的值和对对象b的引用。

结果,如果需要在lambda函数内部将数据转换为其他格式,则可以使用上述机制。 考虑一个例子:

 std::vector<int> data = {2, 4, 4, 1, 1, 3, 9}; int factor = 7; for_each(begin(data), end(data), [&factor](int &val) { //    factor     val = val * factor;   factor--; // #1   - ,  -    factor   }); for(int val: data) {   std::cout << val << ' '; // 14 24 20 4 3 6 9 } 

在这里,如果通过值访问了factor变量(然后使用变量mask [factor]来描述lambda表达式),则在#1行中, factor值无法更改-仅仅是因为我们无权访问执行这样的操作。 在此示例中,我们有权采取此类行动。 在这种情况下,重要的是不要滥用访问变量通过引用提供的功能。

另外,请注意, val也可以通过引用来访问。 这样可以确保lambda函数中发生的数据更改会影响vector

if和switch构造内部的变量初始化表达式


在发现它之后,我真的很喜欢C ++ 17创新。 考虑一个例子:

 std::set<int> input = {1, 5, 3, 6}; if(auto it = input.find(7); it==input.end()){ //   - ,  -    std::cout << 7 << " not found" << std:endl; } else {   //    else      it   std::cout << 7 << " is there!" << std::endl; } 

事实证明,现在可以在一个ifswitch块中初始化变量并与它们的使用进行比较。 这有助于编写准确的代码。 这是所考虑结构的示意图:

 if( init-statement(x); condition(x)) {   //    } else {   //     x   //    } 

使用constexpr执行编译时计算


constexpr为我们提供了巨大的机会。 假设我们需要某种表达式,但是用相应的变量将其值初始化后,其值不会改变。 可以预先计算这样的表达式并将其用作宏。 或者,在C ++ 11中可能使用constexpr

程序员努力使程序执行期间执行的计算量最小化。 结果,如果可以在编译过程中执行某些操作,从而在程序执行过程中减轻了系统负担,那么这将对程序在执行过程中的行为产生良好的影响。 这是一个例子:

 #include <iostream> constexpr long long fact(long long n) { //       constexpr return n == 1 ? 1 : (fact(n-1) * n); } int main() { const long long bigval = fact(20); std::cout<<bigval<<std::endl; } 

这是使用constexpr的非常常见的示例。

由于我们将计算阶乘的函数声明为constexpr ,因此编译器可以在程序编译时预先计算fact(20)值。 结果,编译后,字符串const long long bigval = fact(20); 可以用const long long bigval = 2432902008176640000;

请注意,传递给函数的参数由常量表示。 这是使用通过constexpr声明的函数的重要功能。 传递给它们的参数还必须使用constexprconst关键字声明。 否则,此类函数的行为将与普通函数类似,也就是说,在编译期间,它们的值将不会预先计算。

也可以使用constexpr声明变量。 如您所料,在这种情况下,必须在编译时计算这些变量的值。 如果无法完成,将显示编译错误消息。

有趣的是,后来在C ++ 17中,出现了constexpr-ifconstexpr-lambda构造。

元组数据结构


pair数据结构一样, tuple数据结构(元组)是固定大小的不同类型的值的集合。 这是一个例子:

 auto user_info = std::make_tuple("M", "Chowdhury", 25); //  auto     //    std::get<0>(user_info); std::get<1>(user_info); std::get<2>(user_info); //  C++ 11     tie std::string first_name, last_name, age; std::tie(first_name, last_name, age) = user_info; //  C++ 17, ,       auto [first_name, last_name, age] = user_info; 

有时候,代替tuple数据结构,使用std::array更方便。 此数据结构类似于C语言中使用的简单数组,并具有C ++标准库中的其他功能。 该数据结构出现在C ++ 11中。

自动推断类模板参数的类型


此功能的名称看起来很长很复杂,但实际上,这里没有什么复杂的。 这里的主要思想是,在C ++ 17中,还对标准类模板执行模板参数类型的输出。 以前,仅功能模板支持此功能。 结果,事实证明他们曾经这样写:

 std::pair<std::string, int> user = {"M", 25}; 

随着C ++ 17的发布,现在可以用以下结构代替该结构:

 std::pair user = {"M", 25}; 

类型推断是隐式完成的。 对于元组,此机制更加方便使用。 即,在我不得不编写以下代码之前:

 std::tuple<std::string, std::string, int> user ("M", "Chy", 25); 

现在,同一件事看起来像这样:

 std::tuple user2("M", "Chy", 25); 

值得注意的是,对于那些不太熟悉C ++模板的人来说,这些功能似乎不值得关注。

智能指针


在C ++中使用指针可能是一场噩梦。 由于语言给程序员带来了自由,所以正如他们所说的那样,有时候对他来说很难做到“不让自己陷入困境”。 在许多情况下,指针正在推动程序员的这种“努力”。

幸运的是,C ++ 11引入了智能指针,它比常规指针方便得多。 它们通过尽可能释放资源来帮助程序员避免内存泄漏。 另外,它们为异常提供了安全保证。

总结


这是一个很好的存储库,我们相信,那些遵循C ++创新的人会很感兴趣。 这种语言不断出现新的事物。 在这里,我们仅涉及该语言的一些现代功能。 实际上,有很多。 我们可能仍在谈论它们。

亲爱的读者们! 您发现哪些现代C ++功能最有趣和有用?

Source: https://habr.com/ru/post/zh-CN451870/


All Articles