C++11容易遗忘的特性整理与理解

decltype

decltype是C++11引入的一个关键字,用于获取表达式的类型而不实际执行该表达式。它是一种类型推导工具,可以在编译时确定表达式的类型,而不需要在运行时进行实际计算。

使用decltype,我们可以在编译时获取变量、函数、表达式等的类型,并将其作为一个类型来使用。这在一些复杂的类型推导场景中非常有用,特别是在模板编程中。

decltype的语法如下:

decltype(expression)

其中,expression可以是任何合法的C++表达式,包括变量、函数调用、算术运算、成员访问等。

以下是一些使用decltype的例子:

#include <iostream>
#include <vector>

int main() {
    int x = 5;
    int& ref = x;

    // 使用 decltype 获取 x 的类型
    decltype(x) y; // y 的类型为 int

    // 使用 decltype 获取 ref 的类型
    decltype(ref) z = y; // z 的类型为 int&,与 ref 类型一致

    std::vector<double> vec = {1.2, 3.4, 5.6};

    // 使用 decltype 获取 vec 的类型
    decltype(vec) anotherVec; // anotherVec 的类型为 std::vector<double>

    return 0;
}

在上述示例中,我们使用decltype获取了变量x、引用ref和容器vec的类型,并将这些类型分别赋值给变量y、变量z和变量anotherVecdecltype根据表达式的类型来推导变量的类型,这使得我们能够在不知道表达式类型的情况下编写更通用的代码,特别是在泛型编程中非常有用。

委托/继承构造函数

委托构造函数(Delegating Constructors)和继承构造函数(Inherited Constructors)都是C++11引入的构造函数的特性,它们分别用于简化构造函数的实现和继承基类的构造函数。

  1. 委托构造函数(Delegating Constructors): 委托构造函数允许一个构造函数调用另一个构造函数,在构造函数的初始化列表中使用其他构造函数的功能,从而避免了代码的重复。
class MyClass {
public:
    // 委托构造函数,调用带参数的构造函数
    MyClass(int value) : MyClass(value, 0) {}

    // 带两个参数的构造函数
    MyClass(int value, int otherValue) : value(value), otherValue(otherValue) {}

private:
    int value;
    int otherValue;
};

在上述示例中,我们定义了两个构造函数,一个是带两个参数的构造函数,另一个是委托构造函数。委托构造函数使用了带两个参数的构造函数,并在初始化列表中调用了该构造函数,将传入的value参数作为value成员的值,而将otherValue参数作为otherValue成员的值。这样,我们在编写构造函数时避免了重复的代码,提高了代码的可维护性。

  1. 继承构造函数(Inherited Constructors): 继承构造函数允许派生类继承其基类的构造函数。通过使用继承构造函数,我们可以在派生类中直接调用基类的构造函数,从而避免在派生类中重新实现相同的构造函数。
class Base {
public:
    Base(int value) : baseValue(value) {}
private:
    int baseValue;
};

class Derived : public Base {
public:
    using Base::Base; // 继承基类的构造函数
private:
    // Derived 类添加其他成员和函数
};

在上述示例中,派生类Derived使用using Base::Base语句来继承基类Base的构造函数。这意味着在Derived类中可以直接调用Base类的构造函数,不需要重新实现一个相同的构造函数。通过继承构造函数,我们可以简化代码,提高代码的可读性和维护性。

委托构造函数允许构造函数调用其他构造函数以避免重复代码,而继承构造函数允许派生类继承基类的构造函数,简化派生类的构造函数实现。这些特性在C++11中提供了更加灵活和高效的构造函数使用方式。

模板特化/偏特化

模板特化(Template Specialization)和偏特化(Partial Specialization)是C++编程中与模板相关的重要概念。

  1. 模板特化(Template Specialization): 模板特化是指针对特定类型或特定条件提供自定义实现的一种模板编程技术。当模板被实例化时,如果传入的类型或条件与模板特化的类型或条件匹配,编译器会选择使用特化版本的实现而不是通用的模板实现。

例如,假设我们有一个通用的模板函数用于计算平方:

template <typename T>
T Square(T num) {
    return num * num;
}

现在,如果我们想针对某些特定类型提供更高效的实现,比如针对int类型,我们可以进行模板特化:

template<>
int Square<int>(int num) {
    return num * num;
}

这样,当调用Square函数时,如果传入的参数是int类型,编译器会选择使用模板特化版本的实现。

  1. 偏特化(Partial Specialization): 偏特化是指在模板编程中,针对特定类型或一组类型进行部分定制化的模板实现。偏特化的主要用途是处理模板中的复杂类型或容器类。

偏特化允许你对模板参数进行更精细的匹配,以便提供特定类型的实现。与模板特化不同的是,偏特化中仍保留了模板的通用性,只是对特定类型进行了特殊处理。

举个例子,假设我们有一个通用的模板类用于包装指针:

template <typename T>
class PointerWrapper {
    T* ptr;
public:
    PointerWrapper(T* p) : ptr(p) {}
    // ...
};

然后,我们可以对指针类型进行偏特化,以提供针对某些特定指针的定制化实现:

template <typename T>
class PointerWrapper<T*> {
    T* ptr;
public:
    PointerWrapper(T* p) : ptr(p) {
        // 在这里可以进行针对指针类型的特殊处理
    }
    // ...
};

这样,在创建PointerWrapper对象时,如果传入的是指针类型,编译器会优先选择偏特化版本的实现。

总结: 模板特化用于提供对特定类型的完全定制化实现,而偏特化用于对特定类型进行部分定制化实现,同时保留模板的通用性。这些技术在C++模板编程中非常有用,能够帮助我们实现更灵活和高效的代码。

右值引用、移动语义(move)、完美转发

右值引用、移动语义(move semantics)和完美转发(perfect forwarding)是C++11引入的重要特性,用于优化资源管理和减少不必要的拷贝操作。

  1. 右值引用(Rvalue Reference): 右值引用是一种特殊的引用类型,使用双&&符号表示。它主要用于标识临时对象(右值),也就是那些即将被销毁的对象或者没有命名的临时表达式。与之相对应的是左值引用(单&符号),用于标识有名称的对象。
int x = 42;       // x 是左值
int&& y = 42;     // y 是右值引用,可以绑定到临时对象

右值引用的引入为移动语义和完美转发提供了基础。

  1. 移动语义(Move Semantics): 移动语义是一种新的语义,允许在对象之间转移资源的所有权而不是进行拷贝。通过使用移动语义,可以将资源从一个临时对象(右值)转移到另一个对象,从而避免不必要的深拷贝操作,提高程序性能。

在移动语义中,通过定义移动构造函数和移动赋值运算符,可以实现资源的高效转移。

class MyClass {
public:
    // 移动构造函数
    MyClass(MyClass&& other) {
        // 在此进行资源转移,而不是进行深拷贝
    }

    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) {
        // 在此进行资源转移,而不是进行深拷贝
        return *this;
    }
};
  1. 完美转发(Perfect Forwarding): 完美转发是一种技术,允许在函数中将参数按照原样传递到其他函数,而不会丢失其值类别(左值还是右值)。

在C++中,如果只用左值引用作为参数类型,那么无法接收右值参数。而如果只用右值引用作为参数类型,又无法接收左值参数。完美转发允许我们在模板函数中接收任意类型的参数,并将其原样传递到其他函数,保留参数的值类别。

// 完美转发的示例
template <typename T>
void Forwarder(T&& arg) {
    // arg 在这里既可以是左值引用,也可以是右值引用
    OtherFunction(std::forward<T>(arg));
}

在这里,std::forward就是用于实现完美转发的标准库函数,它根据arg的值类别将其转发给OtherFunction,保留了其原有的值类别。

总结: 右值引用允许我们标识和操作临时对象,移动语义使资源的转移更高效,而完美转发允许我们在函数中接收和传递参数时保留其原有的值类别。这些特性共同为C++带来了更高效和灵活的编程方式。

右值引用、移动语义(move)、完美转发在Qt5中的使用

在Qt5中,右值引用、移动语义和完美转发这些C++11特性都得到了广泛的应用。Qt库本身在Qt5版本中对C++11特性提供了较好的支持,并且在Qt的API和设计中也积极利用这些特性以提高性能和编程便利性。

  1. 右值引用: 在Qt5中,许多函数的参数和返回值类型使用了右值引用,特别是对于大型数据结构或资源管理类,以提高性能并避免不必要的拷贝。例如,Qt中的QVectorQListQHash等容器类都使用了右值引用来支持移动语义。
  2. 移动语义(Move Semantics): Qt5中,对于那些可以转移资源所有权的类,如QScopedPointerQSharedPointer,都实现了移动构造函数和移动赋值运算符,以支持资源的高效转移。这在Qt中的智能指针管理中尤为重要。
  3. 完美转发(Perfect Forwarding): Qt5库中使用了完美转发来提高函数的灵活性和通用性。在Qt的信号与槽机制中,信号和槽的参数使用了完美转发,使得我们可以将任意类型的参数传递给槽函数,并保留其原有的值类别。

例如,QObject::connect函数的参数中就使用了完美转发:

bool QObject::connect(const QObject *sender, PointerToMemberFunction signal,
                      const QObject *receiver, PointerToMemberFunction method,
                      Qt::ConnectionType type = Qt::AutoConnection);

其中,signalmethod参数使用了模板类型,允许我们传递任意类型的成员函数指针。

另外,Qt5还引入了Q_DECLARE_MOVE宏和Q_DISABLE_COPY_MOVE宏来方便地声明和禁用类的拷贝构造函数和移动构造函数,帮助开发者更方便地使用移动语义。

总体而言,Qt5中对右值引用、移动语义和完美转发的支持使得开发者能够更加高效地使用Qt库,提高程序的性能和编程的便利性。

在Qt5中的移动语义、完美转发的代码示例:

  1. 移动语义(Move Semantics)示例:
#include <QtCore/QVector>
#include <QtCore/QDebug>

// 使用移动语义进行资源的转移
void moveResource(QVector<int>&& data) {
    // 移动构造函数将资源从临时对象转移到 dataVector
    QVector<int> dataVector = std::move(data);

    // 在此处使用 dataVector
    qDebug() << "Data Size:" << dataVector.size();
}

int main(int argc, char *argv[]) {
    QVector<int> data = {1, 2, 3, 4, 5};

    // 使用 std::move 显式将 data 转为右值,调用移动语义
    moveResource(std::move(data));

    // 此时 data 不再拥有资源,可以被安全地使用或销毁
    qDebug() << "Data Size After Move:" << data.size(); // Output: Data Size After Move: 0

    return 0;
}

在上面的例子中,我们定义了一个函数moveResource,它接收一个右值引用参数data,并在函数内部使用std::move将资源从临时对象转移到dataVector。通过这样的操作,我们避免了不必要的数据拷贝,提高了程序的性能。

  1. 完美转发(Perfect Forwarding)示例:
#include <QtCore/QDebug>
#include <QtCore/QMetaObject>

// 一个接收任意类型参数的函数
template <typename T>
void processData(T&& data) {
    // 使用完美转发将参数原样传递给槽函数
    QMetaObject::invokeMethod(this, "slotProcessData", Qt::QueuedConnection,
                              Q_ARG(T, std::forward<T>(data)));
}

// 槽函数,接收任意类型参数
void slotProcessData(const QString& str) {
    qDebug() << "Processing QString:" << str;
}

void slotProcessData(int value) {
    qDebug() << "Processing int:" << value;
}

int main(int argc, char *argv[]) {
    processData(QString("Hello")); // Output: Processing QString: "Hello"
    processData(42); // Output: Processing int: 42

    return 0;
}

在上述代码中,我们定义了一个模板函数processData,它使用完美转发将参数data原样传递给槽函数slotProcessData。槽函数可以接收不同类型的参数,并根据传入的实际类型来处理数据。

通过使用完美转发,我们可以在模板函数中接收任意类型的参数,并将其传递给其他函数,保留了参数的原有值类别。在Qt的信号与槽机制中,完美转发可以实现更加通用和灵活的槽函数处理。

变长参数模板

变长参数模板(Variadic Templates)是C++11引入的一个重要特性,它允许定义可以接受任意数量和类型的参数的模板函数或类模板。

在传统的C++中,如果我们想定义一个可以接受不同数量参数的函数,通常需要使用函数重载或使用参数的默认值。然而,这种方式在参数数量很多时会变得非常繁琐,而且无法应对任意数量的参数。

变长参数模板解决了这个问题,它使用递归展开的方式,允许我们在模板中定义接受可变数量参数的函数。

使用语法...来表示参数的变长。在模板中,...可以用于函数参数列表、类模板的参数列表和模板特化。

以下是一个简单的变长参数模板示例:

#include <iostream>

// 递归展开的基准情况:当没有参数时终止递归
void printArgs() {}

// 变长参数模板函数
template<typename T, typename... Args>
void printArgs(const T& arg, const Args&... args) {
    std::cout << arg << " ";
    printArgs(args...); // 递归调用,展开参数列表
}

int main() {
    printArgs(1, "Hello", 3.14, "World"); // Output: 1 Hello 3.14 World
    return 0;
}

在上述示例中,我们定义了一个printArgs函数,它使用变长参数模板来接收任意数量的参数,并通过递归展开的方式依次输出每个参数。在递归过程中,每次我们都取出一个参数,并输出它,直到没有参数为止。

使用变长参数模板,我们可以编写更加灵活和通用的代码,处理不同数量和类型的参数,避免了重复的代码和函数重载。它在许多C++标准库和框架中得到广泛的应用,比如在STL中的std::tuplestd::make_tuple等都使用了变长参数模板。

使用变长参数模板实现可变长参数的整数加法:

#include <iostream>

// 递归展开的基准情况:当没有参数时返回0
int sumArgs() {
    return 0;
}

// 变长参数模板函数,实现不定参数的整数加法
template<typename T, typename... Args>
int sumArgs(const T& arg, const Args&... args) {
    return arg + sumArgs(args...); // 递归调用,展开参数列表并累加
}

int main() {
    int result = sumArgs(1, 2, 3, 4, 5);
    std::cout << "Sum: " << result << std::endl; // Output: Sum: 15

    return 0;
}

在上述示例中,我们定义了一个sumArgs函数,它使用变长参数模板来接收不定数量的整数,并通过递归展开的方式进行累加。在递归过程中,每次我们都取出一个整数并将其加到前面的结果上,直到没有参数为止。这样,我们就实现了一个不定参数的整数加法函数。

注意:在实际应用中,对于不定数量的参数,还应该考虑边界条件、异常处理等,上述示例仅为演示变长参数模板的基本用法。

default、delete、override、final、mutable、volatile的使用场景

这些关键字是C++11及以后版本引入的,用于对类和成员函数进行修饰和标记,提供更加精确和安全的语义。以下是这些关键字的使用场景:

  1. default:用于指示编译器生成默认的特殊成员函数(默认构造函数、析构函数、拷贝构造函数、移动构造函数和赋值运算符)实现。当我们在类中声明了自定义的构造函数或析构函数时,如果还希望编译器生成默认的特殊成员函数,则可以使用default关键字。
class MyClass {
public:
    // 自定义构造函数
    MyClass(int x) { /* custom constructor */ }

    // 使用 default 关键字指示编译器生成默认的拷贝构造函数
    MyClass(const MyClass& other) = default;
};
  1. delete:用于指示删除特殊成员函数或普通函数的定义,从而禁用对该函数的调用。使用delete关键字可以在编译阶段强制限制某些函数的使用。
class MyClass {
public:
    // 使用 delete 关键字禁用默认构造函数
    MyClass() = delete;

    // 自定义拷贝构造函数,但禁用移动构造函数
    MyClass(const MyClass& other) { /* custom copy constructor */ }
    MyClass(MyClass&& other) = delete;
};
  1. override:用于显式地标记重写(覆盖)父类中的虚函数。通过使用override关键字,我们可以确保该函数确实是在派生类中重写了基类中的虚函数,防止由于函数签名不匹配导致的错误。
class Base {
public:
    virtual void foo() {}
};

class Derived : public Base {
public:
    // 使用 override 关键字标记重写基类中的虚函数 foo
    void foo() override {}
};
  1. final:用于阻止派生类对特定虚函数的进一步重写。当在基类中的虚函数声明使用了final关键字时,任何尝试在派生类中再次重写该虚函数的行为都将被编译器禁止。
class Base {
public:
    virtual void foo() final {}
};

class Derived : public Base {
public:
    // 尝试重写基类的 foo,但由于基类中已使用 final 关键字,此处会导致编译错误
    // void foo() override {}
};
  1. mutable:用于标记类的成员变量,即使在const成员函数中,这些被标记为mutable的成员变量仍然可以被修改。这允许在const成员函数中对特定的成员变量进行修改。
class MyClass {
public:
    // 使用 mutable 关键字标记 count 成员变量
    mutable int count = 0;

    void incrementCount() const {
        // 即使在 const 成员函数中,也可以修改被标记为 mutable 的成员变量
        count++;
    }
};
  1. volatile:用于修饰变量,指示编译器在访问该变量时不进行优化,以防止编译器对该变量的优化可能导致的意外结果。volatile通常用于指示该变量可能在不同线程或中断处理程序中被修改,因此在每次访问时都应重新读取该变量的值。
volatile int sharedVariable; // 声明一个被标记为 volatile 的变量

请注意,关键字的使用场景和限制应根据具体的情况选择和使用,合理使用这些关键字可以增强代码的可读性、安全性和性能。

constexpr

constexpr是一个修饰符,用于声明一个表达式或函数在编译时就可以得到计算结果,而不是在运行时进行计算。这样的表达式或函数在编译时会被求值,并将结果用于编译期间的计算,从而提高程序的性能和灵活性。

constexpr关键字通常用于定义常量、数组大小、枚举等,以及一些简单的计算逻辑。在C++11之前,这些常量通常是通过宏定义或全局常量来实现。使用constexpr关键字,可以在编译时计算这些值,而不需要在运行时进行计算。

例如,我们可以定义一个常量值并用于数组的大小:

constexpr int arraySize = 10; // 常量数组大小

int myArray[arraySize]; // 使用常量值作为数组大小

又或者,我们可以使用constexpr关键字定义一个简单的函数,以在编译时计算其结果:

constexpr int add(int x, int y) {
    return x + y;
}

int result = add(5, 7); // 在编译时就计算出 add(5, 7) 的结果并赋值给 result

nullptr是一个空指针常量,用于表示空指针,并解决了与整数0混淆的问题。constexpr是一个修饰符,用于声明编译时可以计算的常量、函数等,并在编译时求值,提高程序的性能和灵活性。这两个关键字在C++中提供了更安全、高效和灵活的编程方式。

posted @ 2023-07-20 11:00  非法关键字  阅读(77)  评论(0编辑  收藏  举报