你应该知道的 7 个高级 C++ 概念
所以,我前段时间开始学习 Modern C++,而且由于我的文章《21 个可在项目中使用的 Modern C++ 新特性》和《关于 C++ 中的 lambda 函数》很受欢迎,所以我决定写一些关于我从这本维基百科和课程中学到的高级 C++ 概念和惯用法的文章。
/!\: 本文最初发表于我的博客。如果您有兴趣接收我的最新文章,请订阅我的电子报。
C++ 还有许多其他高级概念和惯用法,但我认为这 7 个是“应该掌握的”。为了解释它们,我采取了一种更务实而非复杂的方法,更注重可读性和简洁性,而不是其他花哨的功能、语法糖和复杂性。
注:使用这些技术也有一些缺点,我在这里没有讨论,或者可能是我资历不够。
1. RAII
目的:确保在作用域结束时释放资源。
实现:将资源封装到一个类中;资源在构造函数中分配后立即获取;并在析构函数中自动释放;资源通过该类的接口使用;
也称为:循环执行对象、资源释放即终结、作用域绑定资源管理
问题
- 资源获取即初始化习语是最强大、使用最广泛的习语,尽管这个名称很糟糕,因为该习语更多的是关于资源释放而不是获取。
- RAII 保证在作用域/销毁操作结束时释放资源。因此,它确保不会发生资源泄漏,并提供基本的异常安全保障。
struct resource
{
resource(int x, int y) { cout << "resource acquired\n"; }
~resource() { cout << "resource destroyed\n"; }
};
void func()
{
resource *ptr = new resource(1, 2);
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
throw 0; // the function returns early, and ptr won't be deleted!
if (x < 0)
return; // the function returns early, and ptr won't be deleted!
// do stuff with ptr here
delete ptr;
}
- 在上面的代码中,过早的
returnorthrow语句导致函数在未被ptr删除的情况下终止。 - 因此,分配给该变量的内存
ptr现在发生了泄漏(每次调用此函数并提前返回时都会再次发生泄漏)。
解决方案
template<class T>
class smart_ptr
{
T* m_ptr;
public:
template<typename... Args>
smart_ptr(Args&&... args) : m_ptr(new T(std::forward<Args>(args)...)){}
~smart_ptr() { delete m_ptr; }
smart_ptr(const smart_ptr& rhs) = delete;
smart_ptr& operator=(const smart_ptr& rhs) = delete;
smart_ptr(smart_ptr&& rhs) : m_ptr(exchange(rhs.m_ptr, nullptr)){}
smart_ptr& operator=(smart_ptr&& rhs){
if (&rhs == this) return *this;
delete m_ptr;
m_ptr = exchange(rhs.m_ptr,nullptr);
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
void func()
{
auto ptr = smart_ptr<resource>(1, 2); // now ptr guarantee the release of resource
// ...
}
- 请注意,无论
ptr声明之后发生什么,ptr当函数终止时(无论函数如何终止),该变量都会被销毁。 - 由于该 对象
ptr是局部对象,因此在函数栈帧回滚期间会调用析构函数。因此,我们可以确保该对象resource会被正确清理。
用例
- 使用 RAII,可以轻松管理诸如
new/delete、malloc/free、获取/释放、互斥锁锁定/解锁、文件打开/关闭、计数++/ 、数据库连接/断开等资源,或任何其他供应有限的资源。-- - C++ 标准库中的示例包括
std::unique_ptr、std::ofstream、std::lock_guard等。
2. 返回类型解析器
目的:推断被初始化或赋值对象的类型。
实现:使用模板化的转换运算符。
也称为:返回类型重载。
问题
int from_string(const char *str) { return std::stoi(str); }
float from_string(const char *str) { return std::stof(str); } // error
- 函数不能仅通过返回类型进行重载。
解决方案
class from_string
{
const string m_str;
public:
from_string(const char *str) : m_str(str) {}
template <typename type>
operator type(){
if constexpr(is_same_v<type, float>) return stof(m_str);
else if (is_same_v<type, int>) return stoi(m_str);
}
};
int n_int = from_string("123");
float n_float = from_string("123.111");
// Will only work with C++17 due to `is_same_v`
如果你还不了解 constexpr,我写了一篇关于何时在 c++ 中使用 const 与 constexpr 的简短文章。
用例
- 当您使用
nullptr(在 C++11 中引入)时,这是在底层运行的技术,可以根据它所赋值的指针变量推断出正确的类型。 - 如上所示,您还可以根据返回类型来克服函数重载的限制。
- 返回类型解析器还可以用于提供通用的赋值接口,而与赋值的对象无关。
3. 类型擦除
目的:创建一个能够处理各种具体类型的通用容器。
实现方式:可以通过void*模板、多态、联合、代理类等方式
实现。也称为:鸭子类型。
问题
- C++ 是一种静态类型语言,具有强类型特性。在静态类型语言中,对象类型在编译时就已经确定。而在动态类型语言中,类型则与运行时值相关联。
- 在强类型语言中,对象的类型在编译后不会改变。
- 为了克服这一限制并提供动态类型语言等特性,库设计者提出了各种通用容器之类的东西,如
std::any(C++17)、std::variant(C++17)、std::function(C++11)等。
不同类型的擦除技术
- 如何运用这种习语并没有严格的规则,它可以有多种形式,每种形式都有其自身的缺点,如下所示:
=> 使用 void* 进行类型擦除(类似于 C 语言)
void qsort (void* base, size_t num, size_t size,
int (*compare)(const void*,const void*));
缺点:不安全,并且每种类型都需要单独的比较函数
=> 使用模板进行类型擦除
template <class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last);
缺点:可能导致函数模板实例化过多,编译时间延长
=> 使用多态进行类型擦除
struct base { virtual void method() = 0; };
struct derived_1 : base { void method() { cout << "derived_1\n"; } };
struct derived_2 : base { void method() { cout << "derived_2\n"; } };
// We don't see a concrete type (it's erased) though can dynamic_cast
void call(base* ptr) { ptr->method(); };
缺点:运行时成本(动态调度、间接寻址、虚函数表等)
=> 使用联合体进行类型擦除
struct Data {};
union U {
Data d; // occupies 1 byte
std::int32_t n; // occupies 4 bytes
char c; // occupies 1 byte
~U() {} // need to know currently active type
}; // an instance of U in total occupies 4 bytes.
缺点:类型不安全
解决方案
- 正如我之前提到的,标准库已经有了这样的通用容器。
- 为了更好地理解类型擦除,让我们实现一个类似这样的例子
std::any:
struct any
{
struct base {};
template<typename T>
struct inner: base{
inner(T t): m_t{std::move(t)} {}
T m_t;
static void type() {}
};
any(): m_ptr{nullptr}, typePtr{nullptr} {}
template<typename T>
any(T && t): m_ptr{std::make_unique<inner<T>>(t)}, typePtr{&inner<T>::type} {}
template<typename T>
any& operator=(T&& t){
m_ptr = std::make_unique<inner<T>>(t);
typePtr = &inner<T>::type;
return *this;
}
private:
template<typename T>
friend T& any_cast(const any& var);
std::unique_ptr<base> m_ptr = nullptr;
void (*typePtr)() = nullptr;
};
template<typename T>
T& any_cast(const any& var)
{
if(var.typePtr == any::inner<T>::type)
return static_cast<any::inner<T>*>(var.m_ptr.get())->m_t;
throw std::logic_error{"Bad cast!"};
}
int main()
{
any var(10);
std::cout << any_cast<int>(var) << std::endl;
var = std::string{"some text"};
std::cout << any_cast<std::string>(var) << std::endl;
return 0;
}
尤其需要注意的是,我们是如何利用空的静态方法inner<T>::type()来确定模板实例类型的any_cast<T>。
用例
- 可以处理函数/方法返回的多种类型的值(虽然这不是推荐的做法)。
4. CRTP
目的:实现静态多态。
实现方式:利用基类模板特殊化。
也称为:倒置继承、静态多态。
问题
struct obj_type_1
{
bool operator<(const value &rhs) const {return m_x < rhs.m_x;}
// bool operator==(const value &rhs) const;
// bool operator!=(const value &rhs) const;
// List goes on. . . . . . . . . . . . . . . . . . . .
private:
// data members to compare
};
struct obj_type_2
{
bool operator<(const value &rhs) const {return m_x < rhs.m_x;}
// bool operator==(const value &rhs) const;
// bool operator!=(const value &rhs) const;
// List goes on. . . . . . . . . . . . . . . . . . . .
private:
// data members to compare
};
struct obj_type_3 { ...
struct obj_type_4 { ...
// List goes on. . . . . . . . . . . . . . . . . . . .
- 对于每个可比较对象,都需要定义相应的比较运算符。这是多余的,因为如果我们有一个可比较对象
operator <,就可以基于它重载其他运算符。 - 因此,
operator <只有该运算符具有类型信息,其他运算符可以为了可重用性而变得类型无关。
解决方案
- 有趣的是,循环模板模式的实现规则很简单,将类型相关的功能和类型无关的功能分开,并使用模板特化将类型无关的功能与基类绑定。
- 上面的句子乍一看可能令人费解。因此,请参考以下针对上述问题的解决方案,以便更清楚地理解:
template<class derived>
struct compare {};
struct value : public compare<value>
{
value(const int x): m_x(x) {}
bool operator<(const value &rhs) const { return m_x < rhs.m_x; }
private:
int m_x;
};
template <class derived>
bool operator>(const compare<derived> &lhs, const compare<derived> &rhs) {
// static_assert(std::is_base_of_v<compare<derived>, derived>); // Compile time safety measures
return (static_cast<const derived&>(rhs) < static_cast<const derived&>(lhs));
}
/* Same goes with other operators
== :: returns !(lhs < rhs) and !(rhs < lhs)
!= :: returns !(lhs == rhs)
>= :: returns (rhs < lhs) or (rhs == lhs)
<= :: returns (lhs < rhs) or (rhs == lhs)
*/
int main()
{
value v1{5}, v2{10};
cout <<boolalpha<< "v1 == v2: " << (v1 > v2) << '\n';
return 0;
}
// Now no need to write comparator operators for all the classes,
// Write only type dependent `operator <` & use CRTP
用例
- CRTP 被广泛用于实现静态多态,而无需承担虚函数分发机制的开销。考虑以下代码,我们没有使用 virtual 关键字,仍然实现了多态功能(特别是静态多态)。
template<typename specific_animal>
struct animal {
void who() { implementation().who(); }
private:
specific_animal& implementation() {return *static_cast<specific_animal*>(this);}
};
struct dog : public animal<dog> {
void who() { cout << "dog" << endl; }
};
struct cat : public animal<cat> {
void who() { cout << "cat" << endl; }
};
template<typename specific_animal>
void who_am_i(animal<specific_animal> & animal) {
animal.who();
}
- 如上所示,CRTP 也可用于优化,它还可以实现代码重用。
更新:上述声明多重比较运算符的小问题将从 C++20 开始通过使用spaceship ( <=>)/三向比较运算符永久解决。
5. 虚拟构造器
意图:在不知道对象具体类型的情况下创建对象的副本或新对象。
实现:利用重载方法和多态赋值。
也称为:工厂方法/设计模式。
问题
- C++ 支持使用基类的虚析构函数实现多态对象销毁。但由于 C++ 不支持虚构造函数和复制构造函数,因此缺少对对象创建和复制的等效支持。
- 此外,除非你知道对象的静态类型,否则你无法创建对象,因为编译器必须知道需要分配多少空间。出于同样的原因,复制对象也需要在编译时知道其类型。
struct animal {
virtual ~animal(){ cout<<"~animal\n"; }
};
struct dog : animal {
~dog(){ cout<<"~dog\n"; }
};
struct cat : animal {
~cat(){ cout<<"~cat\n"; }
};
void who_am_i(animal *who) { // not sure whether dog would be passed here or cat
// How to `create` the object of same type i.e. pointed by who ?
// How to `copy` object of same type i.e. pointed by who ?
delete who; // you can delete object pointed by who
}
解决方案
- 虚拟构造函数技术允许在 C++ 中实现对象的多态创建和复制,它通过使用虚拟方法将对象的创建和复制操作委托给派生类。
- 以下代码不仅实现了虚构造函数(即
create()),而且还实现了虚复制构造函数(即clone())。
struct animal {
virtual ~animal() = default;
virtual std::unique_ptr<animal> create() = 0;
virtual std::unique_ptr<animal> clone() = 0;
};
struct dog : animal {
std::unique_ptr<animal> create() { return std::make_unique<dog>(); }
std::unique_ptr<animal> clone() { return std::make_unique<dog>(*this); }
};
struct cat : animal {
std::unique_ptr<animal> create() { return std::make_unique<cat>(); }
std::unique_ptr<animal> clone() { return std::make_unique<cat>(*this); }
};
void who_am_i(animal *who) {
auto new_who = who->create();// `create` the object of same type i.e. pointed by who ?
auto duplicate_who = who->clone(); // `copy` object of same type i.e. pointed by who ?
delete who; // you can delete object pointed by who
}
用例
- 提供一个通用接口,仅使用一个类即可生成/复制各种类。
6. SFINAE 和 std::enable_if
目的:从一组重载函数中过滤掉那些无法生成有效模板实例的函数。
实现方式:由编译器自动实现,或利用 `std::enable_if` 函数实现。
也称为:
动机
- 替换失败 不是错误, 这是 C++ 编译器在重载解析期间用来过滤掉一些模板函数重载的语言特性(而不是惯用法) 。
- 在函数模板的重载解析过程中,当用显式指定或推导出的类型替换模板参数失败时,该特化将从重载集中丢弃,而不是导致编译错误。
- 当类型或表达式格式错误时,就会发生替换失败。
template<class T>
void func(T* t){ // Single overload set
if constexpr(std::is_class_v<T>){ cout << "T is user-defined type\n"; }
else { cout << "T is primitive type\n"; }
}
int primitive_t = 6;
struct {char var = '4';} class_t;
func(&class_t);
func(&primitive_t);
- 如何创建两个集合(分别基于基本类型和用户自定义类型),使函数的签名相同?
解决方案
template<class T, typename = std::enable_if_t<std::is_class_v<T>>>
void func(T* t){
cout << "T is user-defined type\n";
}
template<class T, std::enable_if_t<std::is_integral_v<T>, T> = 0>
void func(T* t){ // NOTE: function signature is NOT-MODIFIED
cout << "T is primitive type\n";
}
- 上面的代码片段是一个利用 SFINAE 的简短示例
std::enable_if,其中第一个模板实例化将等价于void func<(anonymous), void>((anonymous) * t)第二个模板实例化void func(int * t)。 - 您可以点击上方
std::enable_if链接阅读更多内容。
用例
- 与 SFINAE 一起
std::enable_if,SFINAE 被大量用于模板元编程。 - 标准库中的大多数type_traits工具也利用了 SFINAE。请考虑以下示例:
// Stolen & trimmed from https://stackoverflow.com/questions/982808/c-sfinae-examples.
template<typename T>
class is_class_type {
template<typename C> static char test(int C::*);
template<typename C> static double test(...);
public:
enum { value = sizeof(is_class_type<T>::test<T>(0)) == sizeof(char) };
};
struct class_t{};
int main()
{
cout<<is_class_type<class_t>::value<<endl; // 1
cout<<is_class_type<int>::value<<endl; // 0
return 0;
}
- 如果没有 SFINAE,你会得到一个编译器错误,类似“
0无法转换为非类类型的成员指针int”,因为这两个重载test仅在返回类型方面有所不同。 - 因为
int不是类,所以它不能有类型为的成员指针int int::*。
7. 代理
目的:使用中间件类实现直观的功能。
实现方式:使用临时/代理类。
也称为:( operator []即下标)代理、双重/两次运算符重载。
动机
- 大多数开发者认为这只是关于下标运算符(即
operator[ ]),但我认为在数据交换之间出现的类型是代理。 - 我们上面已经间接地看到了类型擦除(即类
any::inner<>)的一个很好的例子。不过,我认为再举一个例子能让我们对这个习语的理解更加具体。
运算符 [ ] 解
template <typename T = int>
struct arr2D{
private:
struct proxy_class{
proxy_class(T *arr) : m_arr_ptr(arr) {}
T &operator[](uint32_t idx) { return m_arr_ptr[idx]; }
private:
T *m_arr_ptr;
};
T m_arr[10][10];
public:
arr2D::proxy_class operator[](uint32_t idx) { return arr2D::proxy_class(m_arr[idx]); }
};
int main()
{
arr2D<> arr;
arr[0][0] = 1;
cout << arr[0][0];
return 0;
}
用例
- 为了创建直观的功能,例如双重运算符重载
std::any等。
常见问题解答摘要
何时真正使用 RAII?
当您有一系列步骤来完成一项任务,并且两个步骤是理想的,即设置和清理,那么这就是您可以使用 RAII 的地方。
为什么函数不能按返回类型重载?你不能对返回类型进行重载,因为在函数调用表达式中并非必须使用函数的返回值。例如,我可以这样说:get_val();
编译器现在会怎么做?
当输入类型固定但输出类型可能变化时,可以使用返回类型解析器惯用法。
C++中的类型擦除是什么?类型擦除技术用于设计依赖于赋值类型的泛型类型(就像我们在 Python 中所做的那样)。
顺便问一下,你auto现在知道或者能设计一个吗?
- 在通用编程中很有用。-
也可用于处理函数/方法返回值的多种类型(尽管不建议这样做)。
CRTP 指的是一个类 A 有一个基类,而这个基类又是该类 A 自身的一个模板特化。例如,template <class T> class X{...}; class A : public X<A> {...};
它 是 不是很有循环往复的意味?
我认为这个回答非常恰当。
SFINAE是什么?替换失败 不是错误, 这是 C++ 编译器在重载解析期间用来过滤掉一些模板函数重载的语言特性(而不是惯用法) 。
C++中的代理类是什么?代理类是指为另一个类提供修改后的接口的类。
为什么 C++ 中没有虚构造函数?对于每个包含一个或多个虚函数的类,都会创建一个虚函数表(vtable)。每当创建此类的对象时,该对象都包含一个指向相应虚函数表基址的虚指针。每当调用虚函数时,都会使用虚函数表来解析函数地址。
构造函数不能是虚函数,因为当执行类的构造函数时,内存中还没有虚函数表,这意味着还没有定义虚指针。因此,构造函数必须始终是非虚函数。
类似于“为什么 C++ 中没有虚构造函数?”这个问题,这个问题上面已经回答过了。
虚构造函数有哪些使用场景和必要性?使用基类多态方法创建和复制对象(无需知道其具体类型)。
您可能也喜欢这些
有任何建议、疑问或想说的话Hi吗?别担心,只需轻轻一点即可。🖱️