C++语言的15个obscure特性

这篇具有很好参考价值的文章主要介绍了C++语言的15个obscure特性。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

转载

原文链接: Evan Wallace
译文链接: https://www.cnblogs.com/aanbpsd/p/5328636.html

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

1. 方括号的真正含义

方括号的真正含义
用来访问数组元素的ptr[3]其实只是*(ptr + 3)的缩写,与用*(3 + ptr)是等价的,因此反过来与3[ptr]也是等价的,使用3[ptr]是完全有效的代码。

2. 最烦人的解析

最烦人的解析
“most vexing parse”这个词是由Scott Meyers提出来的,因为C++语法声明的二义性会导致有悖常理的行为:

// 这个解释正确?
// 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++标准要求的是第二种解释,即使第一种解释看起来更直观。程序员可以通过包围括号中变量的初始值来消除歧义:

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

第二种情形让人产生二义性的原因是int y = 3;等价于int(y) = 3;

译者注:这一点我觉得有点迷惑,下面是我在g++下的测试用例:

#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)();
}

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

3.替代运算标记符

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

4. 重定义关键字

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

#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 是为了支持重载而加入的一项技术。编译器将目标源文件中的名字进行调整,这样在目标文件符号表中和连接过程中使用的名字和编译目标文件的源程序中的名字不一样,从而实现重载。

5. Placement new

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

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

#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引起的堆遍历的开销。

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

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

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;
}

7.成员函数的引用修饰符

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

#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;
}

8.图灵完备的模板元编程

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

// 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型变量,数据结构内嵌在类型自身。

// 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++模板的编程语言可用性极低,因此请谨慎和少量使用。模板代码很难阅读,编译速度慢,而且因其冗长和迷惑的错误信息而难以调试。

9.指向成员的指针操作符

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

#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对象的库,就使用成员指针操作符,在包装对象时很容易的指向成员。

#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寄存器里,而普通函数的调用约定却是在栈上解析所有的参数。

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

#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”函数来运用右偏移而不是存储指针自身内部的偏移。

10. 静态实例方法

静态实例方法
C++中可以通过实例调用静态方法也可以通过类直接调用。这可以使你不需要更新任何调用点就可以将实例方法修改为静态方法。

struct Foo {
  static void foo() {}
};
 
// These are equivalent
Foo::foo();
Foo().foo();

11.重载++和–

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

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

12.操作符重载和检查顺序

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

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

#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;
}

13.函数作为模板参数

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

#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);
}

14.模板的参数也是模板

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

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也要写,关键是我们这并不能保证两者的数据类型是一致的。我们真的只想要确定数据类型一次即可,所以我们可以强制其不变,但是没有类型参数的列表会引起编译出错:

// 下面编译通不过,因为NetworkStore和MemoryStore缺失类型参数
CachedStore<NetworkStore, int> c;
CachedStore<MemoryStore, int> d;

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

template <template <typename> class Store, typename T>
struct CachedStore2 {
  Store<T> store;
  Cache<T> cache;
};
 
CachedStore2<NetworkStore, int> e;
CachedStore2<MemoryStore, int> f;

15.try块作为函数

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

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) {
  }
};

奇怪的是,这种语法不仅仅局限于构造函数,也可用于其他的所有函数定义。文章来源地址https://www.toymoban.com/news/detail-412305.html

到了这里,关于C++语言的15个obscure特性的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++相比于C语言增加的8个小特性(详解)

    c++的命名空间是为了弥补C语言的坑,比如我们定义一个变量: rand ,就会出现以下问题! c++调整这一缺点,来进行调整,引用了: namespace 使用方式如下(示例): 命名空间定义和使用 1.命名空间里面也可以 嵌套使用 ! 2.如果在同一个工程中,命名空间名字相同,则

    2024年02月03日
    浏览(43)
  • C++ Primer 6.5 特殊用途语言特性 6.6 函数匹配 知识点+练习题

    在给定的作用域中一个形参只能被赋予一次默认实参 局部变量不能作为默认实参,函数结束就销毁,无法当作默认实参。 除此之外,只要表达式的类型可转换成形参所需要的类型,则可以作为默认实参 将函数定义为内联函数,即加上inline,在编译时内联展开代替函数 在编译

    2024年01月22日
    浏览(46)
  • 【转载】R语言 面向对象编程

    转载自:R语言教程 面向对象的编程侧重于数据和对象,而不是程序。面向对象的模型有助于我们对现实生活中的对象进行建模。为了在数据科学领域出类拔萃,掌握面向对象的编程概念很重要。每个程序都有特殊类型的类。在本教程中,将重点讨论R语言中的S3和S4类、泛型函

    2024年01月20日
    浏览(111)
  • [转载]C++ 入门教程(41课时) - 阿里云大学

    本教程是专门为初学者打造的,帮助他们理解与 C++ 编程语言相关的基础到高级的概念。 在您开始练习本教程中所给出的各种实例之前,您需要对计算机程序和计算机程序设计语言有基本的了解。 运行结果: 你可以用 \\\"n\\\" 代替以上代码里的 \\\"endl\\\"。 C++ 是一种静态类型的、编

    2024年02月08日
    浏览(49)
  • 15面向对象特性

    在程序设计中,封装(Encapsulation)是对具体对象的一种抽象,即将某些部分隐藏起来,在程序外部看不到,其含义是其他程序无法调用。要了解封装,离不开“私有化”,就是将类或者是函数中的某些属性限制在某个区域之内,外部无法调用。 封装的作用: 1、保护隐私(把

    2023年04月23日
    浏览(36)
  • 【Linux】15. 文件系统与软硬链接

    在之前的学习过程当中,我们知道当文件被打开后需要加载进内存,第一步为其创建struct file结构体描述其结构(操作系统需要管理被打开的文件:先描述再组织),在通过进程当中的文件描述符指针指向文件描述符表,根据文件描述符的分配规则给新打开的文件分配文件描述符

    2024年02月09日
    浏览(44)
  • Linux 5.15安全特性之landlock

    Landlock是一个在Linux内核中实现的安全模型,它允许进程在较低的特权级别下运行,并限制其对内核和系统资源的访问。它提供了一种细粒度的权限控制机制,可以用于创建沙盒环境和隔离敏感操作。 Landlock的实现基于eBPF(Extended Berkeley Packet Filter)技术,在Linux 5.15内核中引入

    2024年02月05日
    浏览(54)
  • 【现代 C++ 测试工具链(是时候抛弃 gtest/google bench 了)】【转载】

    gtest需要安装有时候带来很多不方便,比如需要经常切换gcc和clang的时候就比较麻烦,安装的gtest可能在另一个编译器下编译不过, 编写跨平台程序的时候需要多次安装gtest,非常不便。另外一个问题是网络原因,下载安装gtest或者git上拉gtest都可能因为网络原因失败。 除了gte

    2024年03月09日
    浏览(28)
  • Xcode 15新特性与iOS 17适配要点

    在 WWDC 23 上 Apple 推出了 Xcode 15,相比较 Xcode 14,它有如下的变化。 项目 安装包的大小继续减小,安装速度继续提升,因为 iOS 的 Components 也需要动态下载安装,否则提示 iOS 17.0 Not Install ,平台不同提示略有不同。 主界面的菜单图标与名称发生变化。 创建 iOS 项目时,增加了

    2024年02月09日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包