C++你不知道的那些事儿—C++语言的15个晦涩特性

这个列表收集了 C++ 语言的一些晦涩(Obscure)特性,是我经年累月研究这门语言的各个方面收集起来的。C++很是庞大,我老是能学到一些新知识。即便你对C++已了如指掌,也但愿你能从列表中学到一些东西。下面列举的特性,根据晦涩程度由浅入深进行排序。node

  • 1. 方括号的真正含义
  • 2. 最烦人的解析
  • 3.替代运算标记符
  • 4. 重定义关键字
  • 5. Placement new
  • 6.在声明变量的同时进行分支
  • 7.成员函数的引用修饰符
  • 8.图灵完备的模板元编程
  • 9.指向成员的指针操做符
  • 10. 静态实例方法
  • 11.重载++和–
  • 12.操做符重载和检查顺序
  • 13.函数做为模板参数
  • 14.模板的参数也是模板
  • 15.try块做为函数

 

方括号的真正含义

用来访问数组元素的ptr[3]其实只是*(ptr + 3)的缩写,与用*(3 + ptr)是等价的,所以反过来与3[ptr]也是等价的,使用3[ptr]是彻底有效的代码。python

 

最烦人的解析

“most vexing parse”这个词是由Scott Meyers提出来的,由于C++语法声明的二义性会致使有悖常理的行为:ios

1
2
3
4
5
6
7
8
9
10
11
// 这个解释正确?
// 1) 类型std::string的变量会经过std::string()实例化吗?
// 2) 一个函数声明,返回一个std::string值并有一个函数指针参数,
// 该函数也返回一个std::string但没有参数?
std::string foo(std::string());
 
// 仍是这个正确?
// 1)类型int变量会经过int(x)实例化吗?
// 2)一个函数声明,返回一个int值并有一个参数,
// 该参数是一个名为x的int型变量吗?
int bar( int (x));

两种情形下C++标准要求的是第二种解释,即便第一种解释看起来更直观。程序员能够经过包围括号中变量的初始值来消除歧义:git

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

第二种情形让人产生二义性的缘由是int y = 3;等价于int(y) = 3;程序员

译者注:这一点我以为有点迷惑,下面是我在g++下的测试用例:编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
using namespace std;
 
int bar( int (x));   // 等价于int bar(int x)
 
string foo(string());  // 等价于string foo(string (*)())
 
string test() {
     return "test" ;
}
 
int main()
{
     cout << bar(2) << endl; // 输出2
     cout << foo(test); // 输出test
     return 0;
}
 
int bar( int (x)) { 
     return x;
}
 
string foo(string (*fun)()) {
     return (*fun)();
}

能正确输出,但若是按做者意思添加上括号后再编译就会报一堆错误:“在此做用域还没有声明”、“重定义”等,还不清楚做者的意图。数组

 

替代运算标记符

标记符and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq, <%, %>, <: 和 :>均可以用来代替咱们经常使用的&&, &=, &, |, ~, !, !=, ||, |=, ^, ^=, {, }, [ 和 ]。在键盘上缺少必要的符号时你可使用这些运算标记符来代替。数据结构

 

重定义关键字

经过预处理器重定义关键字从技术上讲会引发错误,但其实是容许这样作的。所以你可使用相似#define true false 或 #define else来搞点恶做剧。可是,也有它合法有用的时候,例如,若是你正在使用一个很大的库并且须要绕过C++访问保护机制,除了给库打补丁的方法外,你也可 以在包含该库头文件以前关闭访问保护来解决,但要记得在包含库头文件以后必定要打开保护机制!编程语言

1
2
3
4
5
6
7
8
9
#define class struct
#define private public
#define protected public
 
#include "library.h"
 
#undef class
#undef private
#undef protected

注意这种方式不是每一次都有效,跟你的编译器有关。当实例变量没有被访问控制符修饰时,C++只须要将这些实例变量顺序布局便可,因此编译器能够对 访问控制符组从新排序来自由更改内存布局。例如,容许编译器移动全部的私有成员放到公有成员的后面。另外一个潜在的问题是名称重整(name mangling),Microsoft的C++编译器将访问控制符合并到它们的name mangling表里,所以改变访问控制符意味着将破坏现有编译代码的兼容性。函数

译者注:在C++中,Name Mangling 是为了支持重载而加入的一项技术。编译器将目标源文件中的名字进行调整,这样在目标文件符号表中和链接过程当中使用的名字和编译目标文件的源程序中的名字不同,从而实现重载。

 

Placement new

Placement new是new操做符的一个替代语法,做用在已分配的对象上,该对象已有正确的大小和正确的赋值,这包括创建虚函数表和调用构造函数。

译者注:placement new就是在用户指定的内存位置上构建新的对象,这个构建过程不须要额外分配内存,只须要调用对象的构造函数便可。placement new其实是把本来new作的两步工做分开来:第一步本身分配内存,第二步调用类的构造函数在本身已分配的内存上构建新的对象。placement new的好处:1)在已分配好的内存上进行对象的构建,构建速度快。2)已分配好的内存能够反复利用,有效的避免内存碎片问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
 
struct Test {
   int data;
   Test() { cout << "Test::Test()" << endl; }
   ~Test() { cout << "Test::~Test()" << endl; }
};
 
int main() {
   // Must allocate our own memory
   Test *ptr = (Test *) malloc ( sizeof (Test));
 
   // Use placement new
   new (ptr) Test;
 
   // Must call the destructor ourselves
   ptr->~Test();
 
   // Must release the memory ourselves
   free (ptr);
 
   return 0;
}

当在性能关键的场合须要自定义分配器时可使用Placement new。例如,一个slab分配器从单个的大内存块开始,使用placement new在块里顺序分配对象。这不只避免了内存碎片,也节省了malloc引发的堆遍历的开销。

 

在声明变量的同时进行分支

C++包含一个语法缩写,能在声明变量的同时进行分支。看起来既像单个的变量声明也能够有if或while这样的分支条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Event { virtual ~Event() {} };
struct MouseEvent : Event { int x, y; };
struct KeyboardEvent : Event { int key; };
 
void log (Event *event) {
   if (MouseEvent *mouse = dynamic_cast <MouseEvent *>(event))
     std::cout << "MouseEvent " << mouse->x << " " << mouse->y << std::endl;
 
   else if (KeyboardEvent *keyboard = dynamic_cast <KeyboardEvent *>(event))
     std::cout << "KeyboardEvent " << keyboard->key << std::endl;
 
   else
     std::cout << "Event" << std::endl;
}

 

成员函数的引用修饰符

C++11容许成员函数在对象的值类型上进行重载,this指针会将该对象做为一个引用修饰符。引用修饰符会放在cv限定词(译者注:CV限定词有 三种:const限定符、volatile限定符和const-volatile限定符)相同的位置并依据this对象是左值仍是右值影响重载解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
 
struct Foo {
   void foo() & { std::cout << "lvalue" << std::endl; }
   void foo() && { std::cout << "rvalue" << std::endl; }
};
 
int main() {
   Foo foo;
   foo.foo(); // Prints "lvalue"
   Foo().foo(); // Prints "rvalue"
   return 0;
}

 

图灵完备的模板元编程

C++模板是为了实现编译时元编程,也就是该程序能生成其它的程序。设计模板系统的初衷是进行简单的类型替换,可是在C++标准化过程当中忽然发现模板实际上功能十分强大,足以执行任意计算,虽然很笨拙很低效,但经过模板特化的确能够完成一些计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Recursive template for general case
template < int N>
struct factorial {
   enum { value = N * factorial<N - 1>::value };
};
 
// Template specialization for base case
template <>
struct factorial<0> {
   enum { value = 1 };
};
 
enum { result = factorial<5>::value }; // 5 * 4 * 3 * 2 * 1 == 120

C++模板能够被认为是一种功能型编程语言,由于它们使用递归而非迭代并且包含不可变状态。你可使用typedef建立一个任意类型的变量,使用enum建立一个int型变量,数据结构内嵌在类型自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Compile-time list of integers
template < int D, typename N>
struct node {
   enum { data = D };
   typedef N next;
};
struct end {};
 
// Compile-time sum function
template < typename L>
struct sum {
   enum { value = L::data + sum< typename L::next>::value };
};
template <>
struct sum<end> {
   enum { value = 0 };
};
 
// Data structures are embedded in types
typedef node<1, node<2, node<3, end> > > list123;
enum { total = sum<list123>::value }; // 1 + 2 + 3 == 6

固然这些例子没什么用,但模板元编程的确能够作一些有用的事情,好比能够操做类型列表。可是,使用C++模板的编程语言可用性极低,所以请谨慎和少许使用。模板代码很难阅读,编译速度慢,并且因其冗长和迷惑的错误信息而难以调试。

 

指向成员的指针操做符

指向成员的指针操做符可让你在一个类的任何实例上描述指向某个成员的指针。有两种pointer-to-member操做符,取值操做符*和指针操做符->:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;
 
struct Test {
   int num;
   void func() {}
};
 
// Notice the extra "Test::" in the pointer type
int Test::*ptr_num = &Test::num;
void (Test::*ptr_func)() = &Test::func;
 
int main() {
   Test t;
   Test *pt = new Test;
 
   // Call the stored member function
   (t.*ptr_func)();
   (pt->*ptr_func)();
 
   // Set the variable in the stored member slot
   t.*ptr_num = 1;
   pt->*ptr_num = 2;
 
   delete pt;
   return 0;
}

该特征实际上十分有用,尤为在写库的时候。例如,Boost::Python, 一个用来将C++绑定到Python对象的库,就使用成员指针操做符,在包装对象时很容易的指向成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <boost/python.hpp>
using namespace boost::python;
 
struct World {
   std::string msg;
   void greet() { std::cout << msg << std::endl; }
};
 
BOOST_PYTHON_MODULE(hello) {
   class_<World>( "World" )
     .def_readwrite( "msg" , &World::msg)
     .def( "greet" , &World::greet);
}

记住使用成员函数指针与普通函数指针是不一样的。在成员函数指针和普通函数指针之间casting是无效的。例如,Microsoft编译器里的成员 函数使用了一个称为thiscall的优化调用约定,thiscall将this参数放到ecx寄存器里,而普通函数的调用约定倒是在栈上解析全部的参 数。

并且,成员函数指针可能比普通指针大四倍左右,编译器须要存储函数体的地址,到正确父地址(多个继承)的偏移,虚函数表(虚继承)中另外一个偏移的索引,甚至在对象自身内部的虚函数表的偏移也须要存储(为了前向声明类型)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
 
struct A {};
struct B : virtual A {};
struct C {};
struct D : A, C {};
struct E;
 
int main() {
   std::cout << sizeof ( void (A::*)()) << std::endl;
   std::cout << sizeof ( void (B::*)()) << std::endl;
   std::cout << sizeof ( void (D::*)()) << std::endl;
   std::cout << sizeof ( void (E::*)()) << std::endl;
   return 0;
}
 
// 32-bit Visual C++ 2008:  A = 4, B = 8, D = 12, E = 16
// 32-bit GCC 4.2.1:        A = 8, B = 8, D = 8,  E = 8
// 32-bit Digital Mars C++: A = 4, B = 4, D = 4,  E = 4

在Digital Mars编译器里全部的成员函数都是相同的大小,这是源于这样一个聪明的设计:生成“thunk”函数来运用右偏移而不是存储指针自身内部的偏移。

 

静态实例方法

C++中能够经过实例调用静态方法也能够经过类直接调用。这可使你不须要更新任何调用点就能够将实例方法修改成静态方法。

1
2
3
4
5
6
7
struct Foo {
   static void foo() {}
};
 
// These are equivalent
Foo::foo();
Foo().foo();

 

重载++和–

C++的设计中自定义操做符的函数名称就是操做符自己,这在大部分状况下都工做的很好。例如,一元操做符的-和二元操做符的-(取反和相减)能够通 过参数个数来区分。但这对于一元递增和递减操做符却不奏效,由于它们的特征彷佛彻底相同。C++语言有一个很笨拙的技巧来解决这个问题:后缀++和–操做 符必须有一个空的int参数做为标记让编译器知道要进行后缀操做(是的,只有int类型有效)。

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

 

操做符重载和检查顺序

重载,(逗号),||或者&&操做符会引发混乱,由于它打破了正常的检查规则。一般状况下,逗号操做符在整个左边检查完毕才开始检 查右边,|| 和 &&操做符有短路行为:仅在必要时才会去检查右边。不管如何,操做符的重载版本仅仅是函数调用且函数调用以未指定的顺序检查它们的参数。

重载这些操做符只是一种滥用C++语法的方式。做为一个实例,下面我给出一个Python形式的无括号版打印语句的C++实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
 
namespace __hidden__ {
   struct print {
     bool space;
     print() : space( false ) {}
     ~print() { std::cout << std::endl; }
 
     template < typename T>
     print &operator , ( const T &t) {
       if (space) std::cout << ' ' ;
       else space = true ;
       std::cout << t;
       return * this ;
     }
   };
}
 
#define print __hidden__::print(),
 
int main() {
   int a = 1, b = 2;
   print "this is a test" ;
   print "the sum of" , a, "and" , b, "is" , a + b;
   return 0;
}

 

函数做为模板参数

众所周知,模板参数能够是特定的整数也能够是特定的函数。这使得编译器在实例化模板代码时内联调用特定的函数以得到更高效的执行。下面的例子里,函数memoize的模板参数也是一个函数且只有新的参数值才经过函数调用(旧的参数值能够经过cache得到):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <map>
 
template < int (*f)( int )>
int memoize( int x) {
   static std::map< int , int > cache;
   std::map< int , int >::iterator y = cache.find(x);
   if (y != cache.end()) return y->second;
   return cache[x] = f(x);
}
 
int fib( int n) {
   if (n < 2) return n;
   return memoize<fib>(n - 1) + memoize<fib>(n - 2);
}

 

模板的参数也是模板

模板参数实际上自身的参数也能够是模板,这可让你在实例化一个模板时能够不用模板参数就可以传递模板类型。看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template < typename T>
struct Cache { ... };
 
template < typename T>
struct NetworkStore { ... };
 
template < typename T>
struct MemoryStore { ... };
 
template < typename Store, typename T>
struct CachedStore {
   Store store;
   Cache<T> cache;
};
 
CachedStore<NetworkStore< int >, int > a;
CachedStore<MemoryStore< int >, int > b;

CachedStore的cache存储的数据类型与store的类型相同。然而咱们在实例化一个CachedStore必须重复写数据类型(上面 的代码是int型),store自己要写,CachedStore也要写,关键是咱们这并不能保证二者的数据类型是一致的。咱们真的只想要肯定数据类型一 次便可,因此咱们能够强制其不变,可是没有类型参数的列表会引发编译出错:

1
2
3
// 下面编译通不过,由于NetworkStore和MemoryStore缺失类型参数
CachedStore<NetworkStore, int > c;
CachedStore<MemoryStore, int > d;

模板的模板参数可让咱们得到想要的语法。注意你必须使用class关键字做为模板参数(他们自身的参数也是模板)

1
2
3
4
5
6
7
8
template < template < typename > class Store, typename T>
struct CachedStore2 {
   Store<T> store;
   Cache<T> cache;
};
 
CachedStore2<NetworkStore, int > e;
CachedStore2<MemoryStore, int > f;

 

try块做为函数

函数的try块会在检查构造函数的初始化列表时捕获抛出的异常。你不能在初始化列表的周围加上try-catch块,由于其只能出如今函数体外。为了解决这个问题,C++容许try-catch块也可做为函数体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int f() { throw 0; }
 
// 这里没有办法捕获由f()抛出的异常
struct A {
   int a;
   A::A() : a(f()) {}
};
 
// 若是try-catch块被用做函数体而且初始化列表移至try关键字以后的话,
// 那么由f()抛出的异常就能够捕获到
struct B {
   int b;
   B::B() try : b(f()) {
   } catch ( int e) {
   }
};

奇怪的是,这种语法不只仅局限于构造函数,也可用于其余的全部函数定义。

 

译自:http://madebyevan.com/obscure-cpp-features/

相关文章
相关标签/搜索