发布于 2026-01-06 4 阅读
0

7 Advanced C++ Concepts You Should Know

你应该知道的 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;
}
Enter fullscreen mode Exit fullscreen mode
  • 在上面的代码中,过早的 return or throw 语句导致函数在未被 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
    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • 请注意,无论ptr声明之后发生什么,ptr 当函数终止时(无论函数如何终止),该变量都会被销毁。
  • 由于该 对象ptr是局部对象,因此在函数栈帧回滚期间会调用析构函数。因此,我们可以确保该对象 resource 会被正确清理。

用例

  • 使用 RAII,可以轻松管理诸如new/ deletemalloc/ free、获取/释放、互斥锁锁定/解锁、文件打开/关闭、计数++/ 、数据库连接/断开等资源,或任何其他供应有限的资源。--
  • C++ 标准库中的示例包括std::unique_ptrstd::ofstreamstd::lock_guard等。

2. 返回类型解析器

目的:推断被初始化或赋值对象的类型。
实现:使用模板化的转换运算符。
也称为:返回类型重载。

问题

int from_string(const char *str) { return std::stoi(str); }
float from_string(const char *str) { return std::stof(str); } // error
Enter fullscreen mode Exit fullscreen mode
  • 函数不能仅通过返回类型进行重载。

解决方案

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`
Enter fullscreen mode Exit fullscreen mode

如果你还不了解 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*));
Enter fullscreen mode Exit fullscreen mode

缺点:不安全,并且每种类型都需要单独的比较函数

=> 使用模板进行类型擦除

template <class RandomAccessIterator>
  void sort(RandomAccessIterator first, RandomAccessIterator last);
Enter fullscreen mode Exit fullscreen mode

缺点:可能导致函数模板实例化过多,编译时间延长

=> 使用多态进行类型擦除

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(); };
Enter fullscreen mode Exit fullscreen mode

缺点:运行时成本(动态调度、间接寻址、虚函数表等)

=> 使用联合体进行类型擦除

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.
Enter fullscreen mode Exit fullscreen mode

缺点:类型不安全

解决方案

  • 正如我之前提到的,标准库已经有了这样的通用容器。
  • 为了更好地理解类型擦除,让我们实现一个类似这样的例子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;
}
Enter fullscreen mode Exit fullscreen mode

尤其需要注意的是,我们是如何利用空的静态方法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. . . . . . . . . . . . . . . . . . . .
Enter fullscreen mode Exit fullscreen mode
  • 对于每个可比较对象,都需要定义相应的比较运算符。这是多余的,因为如果我们有一个可比较对象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
Enter fullscreen mode Exit fullscreen mode

用例

  • 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();
}
Enter fullscreen mode Exit fullscreen mode
  • 如上所示,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
}
Enter fullscreen mode Exit fullscreen mode

解决方案

  • 虚拟构造函数技术允许在 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
}
Enter fullscreen mode Exit fullscreen mode

用例

  • 提供一个通用接口,仅使用一个类即可生成/复制各种类。

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);
Enter fullscreen mode Exit fullscreen mode
  • 如何创建两个集合(分别基于基本类型和用户自定义类型),使函数的签名相同?

解决方案

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";
}
Enter fullscreen mode Exit fullscreen mode
  • 上面的代码片段是一个利用 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;
}
Enter fullscreen mode Exit fullscreen mode
  • 如果没有 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;
}
Enter fullscreen mode Exit fullscreen mode

用例

  • 为了创建直观的功能,例如双重运算符重载std::any等。

常见问题解答摘要

何时真正使用 RAII?

当您有一系列步骤来完成一项任务,并且两个步骤是理想的,即设置和清理,那么这就是您可以使用 RAII 的地方。

为什么函数不能按返回类型重载?

你不能对返回类型进行重载,因为在函数调用表达式中并非必须使用函数的返回值。例如,我可以这样说:

get_val();

编译器现在会怎么做?

何时使用返回类型解析器惯用法?

当输入类型固定但输出类型可能变化时,可以使用返回类型解析器惯用法。

C++中的类型擦除是什么?

类型擦除技术用于设计依赖于赋值类型的泛型类型(就像我们在 Python 中所做的那样)。
顺便问一下,你auto现在知道或者能设计一个吗?

最适合应用类型擦除惯用法的场景是什么?

- 在通用编程中很有用。-
也可用于处理函数/方法返回值的多种类型(尽管不建议这样做)。

什么是奇异重复模板模式(CRTP)?

CRTP 指的是一个类 A 有一个基类,而这个基类又是该类 A 自身的一个模板特化。例如,
template <class T>
class X{...};
class A : public X<A> {...};
它  不是很有循环往复的意味?

为什么奇特重复模板模式(CRTP)有效?

我认为这个回答非常恰当。

SFINAE是什么?

替换失败 不是错误, 是 C++ 编译器在重载解析期间用来过滤掉一些模板函数重载的语言特性(而不是惯用法)  。 

C++中的代理类是什么?

代理类是指为另一个类提供修改后的接口的类。

为什么 C++ 中没有虚构造函数?

对于每个包含一个或多个虚函数的类,都会创建一个虚函数表(vtable)。每当创建此类的对象时,该对象都包含一个指向相应虚函数表基址的虚指针。每当调用虚函数时,都会使用虚函数表来解析函数地址。
构造函数不能是虚函数,因为当执行类的构造函数时,内存中还没有虚函数表,这意味着还没有定义虚指针。因此,构造函数必须始终是非虚函数。

在 C++ 中,我们可以将类的复制构造函数设为虚函数吗?

类似于“为什么 C++ 中没有虚构造函数?”这个问题,这个问题上面已经回答过了。

虚构造函数有哪些使用场景和必要性?

使用基类多态方法创建和复制对象(无需知道其具体类型)。

您可能也喜欢这些

有任何建议、疑问或想说的话Hi吗?别担心,只需轻轻一点即可。🖱️

文章来源:https://dev.to/visheshpatel/7-advanced-c-concepts-you-should-know-4gog