C++语言的15个晦涩特性

原创
小哥 3年前 (2023-05-24) 阅读数 5 #大杂烩

转自: http://developer.51cto.com/art/201312/425995.htm

此列表已收集 C++ 语言中的一些歧义(Obscure)通过对这种语言各个方面的多年研究,收集了特征。C++它非常巨大,我总能学到一些新知识。即使你是对的C++我知道它就像我的手掌一样,我希望你能从列表中学到一些东西。下面列出的特征根据其模糊程度从浅到深排序。

此列表已收集 C++ 语言中的一些歧义(Obscure)通过对这种语言各个方面的多年研究,收集了特征。C++它非常巨大,我总能学到一些新知识。即使你是对的C++我知道它就像我的手掌一样,我希望你能从列表中学到一些东西。下面列出的特征根据其模糊程度从浅到深排序。

    1. 方括号的真正含义
    1. 最烦人的分析
  • 3.替换操作标记
    1. 重新定义关键字
    1. Placement new
  • 6.声明变量时的分支
  • 7.成员函数的引用修饰符
  • 8.迁移到完整模板元编程
  • 9.指向成员的指针运算符
    1. 静态实例方法
  • 11.重载++和–
  • 12.操作员重载和检查顺序
  • 13.函数作为模板参数
  • 14.模板的参数也是模板
  • 15.try块作为函数

方括号的真正含义

用于访问数组元素ptr[3]其实只是(ptr + 3)缩写,与(3 + ptr)是等价的,因此相反是真的3[ptr]它也是等效的,使用3[ptr]这是完全有效的代码

最烦人的分析

“most vexing parse“这个词来源于:Scott Meyers之所以提出它,是因为C++语法陈述的歧义可能导致不合逻辑的行为:

  1. // 这个解释正确吗? 

  2. // 1) 类型std::string的变量将传递std::string()例示? 

  3. // 2) 返回std::string并具有函数指针参数, 

  4. // 此函数还返回一个std::string但是没有参数? 

  5. std::string foo(std::string()); 

  6. // 这仍然正确吗? 

  7. // 1)类型int变量将传递int(x)例示? 

  8. // 2)返回int具有一个参数的值, 

  9. // 此参数是名为x的int它是一个类型变量吗? 

  10. int bar(int(x)); 

在两种情况下C++该标准需要第二种解释,即使第一种解释看起来更直观。程序员可以通过将变量的初始值括在括号中来消除歧义:

  1. //添加括号以消除歧义 
  2. std::string foo((std::string())); 
  3. int bar((int(x))); 

第二种情况造成歧义的原因是int y = 3;等价于int(y) = 3;

译者注:我对这一点感到有点困惑。以下是我的解释g++测试用例:

  1. include  

  2. include  

  3. using namespace std; 

  4. int bar(int(x));   // 等价于int bar(int x) 

  5. string foo(string());  // 等价于string foo(string (*)()) 

  6. string test() { 

  7.     return "test"; 

  8. int main() 

  9.     cout << bar(2) << endl; // 输出2 

  10.     cout << foo(test); // 输出test 

  11.     return 0; 

  12. int bar(int(x)) {  

  13.     return x; 

  14. string foo(string (*fun)()) { 

  15.     return (*fun)(); 

它可以正确输出,但如果在编译前根据作者的意图添加括号,则会报告一堆错误,例如“尚未在此范围内声明”、“重新定义”等。不清楚作者的意图是什么。

替换操作标记

标记符and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq, <%, %>, <: 和 :>可以用来替代我们常用的&&, &=, &, |, ~, !, !=, ||, |=, ^, ^=, {, }, [ 和 ]。当键盘上缺少必要的符号时,可以改用这些操作标记。

重新定义关键字

通过预处理器重新定义关键字从技术上讲会引起错误,但实际上是允许这样做的。因此你可以使用类似#define true false 或 #define else来玩一些恶作剧。但是,有时它是合法且有用的,例如,如果您正在使用大型库并需要绕过它C++访问保护机制,除了打补丁库的方法外,还可以 要解决此问题,请在包含库头文件之前关闭访问保护,但请记住在包含库头文件后打开保护机制!

  1. define class struct 

  2. define private public 

  3. define protected public 

  4. include "library.h" 

  5. undef class 

  6. undef private 

  7. undef protected 

请注意,此方法并不总是有效的,这取决于您的编译器。当实例变量未使用访问控制字符修饰时,C++只需按顺序布置这些实例变量,以便编译器可以 重新排序访问控制字符组以自由更改内存布局。例如,允许编译器将所有私有成员移动到公共成员之后。另一个潜在的问题是名称重组(name mangling),Microsoft的C++编译器将访问控制字符合并到其name mangling因此,更改表中的访问控制字符意味着破坏现有编译代码的兼容性。

译者注:在C++中,Name Mangling 它是为支持重载而添加的技术。编译器调整目标源文件中的名称,使目标文件的符号表和连接过程中使用的名称与编译目标文件的源程序中的名称不同,从而实现重载。

Placement new

Placement new是new运算符的替代语法应用于分配的对象,该对象具有正确的大小和值赋值,包括 Virtual 函数表的建立和构造函数的调用。

译者注:placement new它是在用户指定的内存位置构建一个新对象,并且此构造过程不需要额外的内存分配。它只需要调用对象的构造函数。placement new其实,这只是把原版放进去的问题new这两个步骤是分开的:第一步是自己分配内存,第二步是调用类构造函数以在分配的内存上构建一个新对象。placement new的好处:1)在分配的内存上构建对象的速度很快。2)分配的内存可以重复使用,有效避免内存碎片问题。

  1. include  

  2. using namespace std; 

  3. struct Test { 

  4.   int data; 

  5.   Test() { cout << "Test::Test()" << endl; } 

  6.   ~Test() { cout << "Test::~Test()" << endl; } 

  7. }; 

  8. int main() { 

  9.   // Must allocate our own memory 

  10.   Test ptr = (Test )malloc(sizeof(Test)); 

  11.   // Use placement new 

  12.   new (ptr) Test; 

  13.   // Must call the destructor ourselves 

  14.   ptr->~Test(); 

  15.   // Must release the memory ourselves 

  16.   free(ptr); 

  17.   return 0; 

可在性能关键情况下需要自定义分配器时使用Placement new。例如,一个slab分配器从单个大内存块开始并使用placement new在块内按顺序分配对象。这不仅避免了内存碎片,而且节省了malloc堆遍历的开销。

声明变量时的分支

C++包含一个语法缩写,该缩写可以在声明变量时分支。它看起来像一个单变量声明,也可以有if或while这样的分支条件。

  1. struct Event { virtual ~Event() {} }; 

  2. struct MouseEvent : Event { int x, y; }; 

  3. struct KeyboardEvent : Event { int key; }; 

  4. void log(Event *event) { 

  5.   if (MouseEvent mouse = dynamic_cast<MouseEvent >(event)) 

  6.     std::cout << "MouseEvent " << mouse->x << " " << mouse->y << std::endl; 

  7.   else if (KeyboardEvent keyboard = dynamic_cast<KeyboardEvent >(event)) 

  8.     std::cout << "KeyboardEvent " << keyboard->key << std::endl; 

  9.   else 

  10.     std::cout << "Event" << std::endl; 

成员函数的引用修饰符

C++11允许成员函数在对象的值类型上重载,this指针会将对象视为引用修饰符。引文修饰符将放置在cv限定符(译者注:CV限定词有 三种:const限定符、volatile限定符和const-volatile限定符)处于同一位置并基于this对象是左值还是左值都会影响重载解析:

  1. include  

  2. struct Foo { 

  3.   void foo() & { std::cout << "lvalue" << std::endl; } 

  4.   void foo() && { std::cout << "rvalue" << std::endl; } 

  5. }; 

  6. int main() { 

  7.   Foo foo; 

  8.   foo.foo(); // Prints "lvalue" 

  9.   Foo().foo(); // Prints "rvalue" 

  10.   return 0; 

迁移到完整模板元编程

C++模板是用来实现编译时元编程的,也就是说,程序可以生成其他程序。设计模板系统的初衷是执行简单的类型替换,但在C++在标准化过程中,突然发现模板实际上非常强大,能够执行任意计算。虽然笨拙且效率低下,但模板专用化确实可以完成一些计算:

  1. // Recursive template for general case 

  2. template <int N> 

  3. struct factorial { 

  4.   enum { value = N * factorial<N - 1>::value }; 

  5. }; 

  6. // Template specialization for base case 

  7. template <> 

  8. struct factorial<0> { 

  9.   enum { value = 1 }; 

  10. }; 

  11. enum { result = factorial<5>::value }; // 5  4  3  2  1 == 120 

C++模板可以被视为一种函数式编程语言,因为它们使用递归而不是迭代,并且包含不可变的状态。您可以使用typedef使用 创建任何类型的变量enum创建一个int类型变量,嵌入在类型本身中的数据结构。

  1. // Compile-time list of integers 

  2. template <int D, typename N> 

  3. struct node { 

  4.   enum { data = D }; 

  5.   typedef N next; 

  6. }; 

  7. struct end {}; 

  8. // Compile-time sum function 

  9. template <typename L> 

  10. struct sum { 

  11.   enum { value = L::data + sum<typename L::next>::value }; 

  12. }; 

  13. template <> 

  14. struct sum { 

  15.   enum { value = 0 }; 

  16. }; 

  17. // Data structures are embedded in types 

  18. typedef node<1, node<2, node<3, end> > > list123; 

  19. enum { total = sum::value }; // 1 + 2 + 3 == 6 

当然,这些示例是无用的,但是模板元编程可以做一些有用的事情,例如操作类型列表。但是,使用C++模板的编程语言可用性极低,因此请谨慎使用它们并少量使用它们。模板代码难以阅读、编译缓慢且难以调试,因为它的错误消息冗长且令人困惑。

指向成员的指针运算符

指向成员的指针运算符允许您在类的任何实例上描述指向成员的指针。有两种类型pointer-to-member算子*和指针运算符->:

  1. include  

  2. using namespace std; 

  3. struct Test { 

  4.   int num; 

  5.   void func() {} 

  6. }; 

  7. // Notice the extra "Test::" in the pointer type 

  8. int Test::*ptr_num = &Test::num; 

  9. void (Test::*ptr_func)() = &Test::func; 

  10. int main() { 

  11.   Test t; 

  12.   Test *pt = new Test; 

  13.   // Call the stored member function 

  14.   (t.*ptr_func)(); 

  15.   (pt->*ptr_func)(); 

  16.   // Set the variable in the stored member slot 

  17.   t.*ptr_num = 1; 

  18.   pt->*ptr_num = 2; 

  19.   delete pt; 

  20.   return 0; 

此功能实际上非常有用,尤其是在编写库时。例如Boost::Python, 一个习惯C++绑定到Python对象库使用成员指针运算符,这使得在包装对象时可以轻松地指向成员。

  1. include  

  2. include <boost/python.hpp> 

  3. using namespace boost::python; 

  4. struct World { 

  5.   std::string msg; 

  6.   void greet() { std::cout << msg << std::endl; } 

  7. }; 

  8. BOOST_PYTHON_MODULE(hello) { 

  9.   class_("World") 

  10.     .def_readwrite("msg", &World::msg) 

  11.     .def("greet", &World::greet); 

请记住,使用成员函数指针不同于使用普通函数指针。在成员函数指针和普通函数指针之间casting它是无效的。例如Microsoft编译器中的成员 该函数使用一个名为thiscall优化调用约定,thiscall将this参数放到ecx在寄存器中,而常规函数的调用约定是解析堆栈上的所有参数 数。

此外,成员函数指针可能比普通指针大四倍左右。编译器需要存储函数体的地址、到正确父地址的偏移量(多个继承)、虚拟函数表中另一个偏移量的索引(虚拟继承),甚至是对象本身内部的 Virtual 函数表的偏移量(用于类型的前向声明)。

  1. include  

  2. struct A {}; 

  3. struct B : virtual A {}; 

  4. struct C {}; 

  5. struct D : A, C {}; 

  6. struct E; 

  7. int main() { 

  8.   std::cout << sizeof(void (A::*)()) << std::endl; 

  9.   std::cout << sizeof(void (B::*)()) << std::endl; 

  10.   std::cout << sizeof(void (D::*)()) << std::endl; 

  11.   std::cout << sizeof(void (E::*)()) << std::endl; 

  12.   return 0; 

  13. // 32-bit Visual C++ 2008:  A = 4, B = 8, D = 12, E = 16 

  14. // 32-bit GCC 4.2.1:        A = 8, B = 8, D = 8,  E = 8 

  15. // 32-bit Digital Mars C++: A = 4, B = 4, D = 4,  E = 4 

在Digital Mars编译器中的所有成员函数都具有相同的大小,这源于生成的智能设计”thunk“使用右偏移量而不是存储指针本身的内部偏移量的函数。

静态实例方法

C++静态方法可以通过实例或直接通过类调用。这允许您将实例方法修改为静态方法,而无需更新任何调用点。

  1. struct Foo { 

  2.   static void foo() {} 

  3. }; 

  4. // These are equivalent 

  5. Foo::foo(); 

  6. Foo().foo(); 

重载++和–

C++设计中自定义运算符的函数名称是运算符本身,在大多数情况下效果很好。例如,一元运算符的-和二进制运算符-(否定和减法)可以通过 通过参数的数量来区分。但这不适用于一元递增和递减运算符,因为它们的特征似乎是相同的。C++语言有一个非常笨拙的技术来解决这个问题:后缀++和–操作 符号必须为空int该参数用作标记,让编译器知道执行后缀操作(是的,仅int键入有效。

  1. struct Number { 
  2.   Number &operator ++ (); // Generate a prefix ++ operator 
  3.   Number operator ++ (int); // Generate a postfix ++ operator 
  4. }; 

操作员重载和检查顺序

重载,(逗号),||或者&&操作员可能会造成混淆,因为它违反了正常的检查规则。通常,逗号运算符仅在检查整个左侧后才开始检查 查右边,|| 和 &&操作员具有短路行为:它只在必要时检查右侧。无论如何,运算符的重载版本只是函数调用,函数调用以未指定的顺序检查其参数。

使这些运算符过载只是一种滥用C++语法方法。作为示例,我将提供一个Python以括号形式打印语句 免费版本C++实现:

  1. include  

  2. namespace __hidden__ { 

  3.   struct print { 

  4.     bool space; 

  5.     print() : space(false) {} 

  6.     ~print() { std::cout << std::endl; } 

  7.     template <typename T> 

  8.     print &operator , (const T &t) { 

  9.       if (space) std::cout <<  ; 

  10.       else space = true; 

  11.       std::cout << t; 

  12.       return *this; 

  13.     } 

  14.   }; 

  15. define print __hidden__::print(), 

  16. int main() { 

  17.   int a = 1, b = 2; 

  18.   print "this is a test"; 

  19.   print "the sum of", a, "and", b, "is", a + b; 

  20.   return 0; 

函数作为模板参数

众所周知,模板参数可以是特定的整数或函数。这允许编译器内联调用特定函数,以便在实例化模板代码时更有效地执行。在以下示例中,函数memoize的模板参数也是一个函数,只能通过函数调用新的参数值(旧的参数值可以通过cache获得):

  1. include  

  2. template <int (*f)(int)> 

  3. int memoize(int x) { 

  4.   static std::map<int, int> cache; 

  5.   std::map<int, int>::iterator y = cache.find(x); 

  6.   if (y != cache.end()) return y->second; 

  7.   return cache[x] = f(x); 

  8. int fib(int n) { 

  9.   if (n < 2) return n; 

  10.   return memoize(n - 1) + memoize(n - 2); 

模板的参数也是模板

模板参数实际上可以是模板本身,这允许您在实例化模板时传递不带模板参数的模板类型。请看下面的代码:

  1. template <typename T> 

  2. struct Cache { ... }; 

  3. template <typename T> 

  4. struct NetworkStore { ... }; 

  5. template <typename T> 

  6. struct MemoryStore { ... }; 

  7. template <typename Store, typename T> 

  8. struct CachedStore { 

  9.   Store store; 

  10.   Cache cache; 

  11. }; 

  12. CachedStore<NetworkStore, int> a; 

  13. CachedStore<MemoryStore, int> b; 

CachedStore的cache存储的数据类型不同于store类型相同。但是,我们正在实例化一个CachedStore数据类型必须重复编写(上面的代码 是int型),store我必须自己写,CachedStore我们还需要写,关键是我们不能保证两者的数据类型是一致的。我们真的只想确定一次数据类型,即 是的,所以我们可以强制它保持不变,但是没有类型参数的列表可能会导致编译错误:

  1. // 以下编译失败,因为NetworkStore和MemoryStore缺少类型参数 
  2. CachedStore<NetworkStore, int> c; 
  3. CachedStore<MemoryStore, int> d; 

模板的模板参数允许我们获得所需的语法。请注意,您必须使用class关键字作为模板参数(它们自己的参数也是模板)

  1. template <template  class Store, typename T> 

  2. struct CachedStore2 { 

  3.   Store store; 

  4.   Cache cache; 

  5. }; 

  6. CachedStore2<NetworkStore, int> e; 

  7. CachedStore2<MemoryStore, int> f; 

try块作为函数

函数的try块会在检查构造函数的初始化列表时捕获引发异常。你不能在初始化列表的周围加上try-catch阻止,因为它只能出现在函数之外。为了解决这个问题,C++允许try-catch块也可以用作功能主体:

  1. int f() { throw 0; } 

  2. // 没有办法捕获f()引发异常 

  3. struct A { 

  4.   int a; 

  5.   A::A() : a(f()) {} 

  6. }; 

  7. // 如果try-catch块用作函数体,初始化列表移动到try在关键字之后, 

  8. // 那么由f()引发异常就可以捕获到 

  9. struct B { 

  10.   int b; 

  11.   B::B() try : b(f()) { 

  12.   } catch(int e) { 

  13.   } 

  14. }; 

奇怪的是,这种语法不仅限于构造函数,还可以用于所有其他函数定义。

原文链接: http://madebyevan.com/obscure-cpp-features/

翻译链接: http://blog.jobbole.com/54140/

版权声明

所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除