C++ loose knowledge by catty

1. Multithreading

Usage of std::thread() and join() : Use std::thread() to create a thread, specify the thread execution function and parameters, and use lamda expressions.

#include <iostream>
#include <thread>

void threadFunction(int num) {
    
    
    std::cout << "Hello from thread with number: " << num << std::endl;
}

int main() {
    
    
    int threadNum = 42;
    std::thread t(threadFunction, threadNum); // 将threadNum传递给线程函数
    t.join(); // 将会阻塞主线程的执行等待t线程执行完毕
	// 如果没有使用join可能造成主线程结束而t线程没执行完毕提前消亡
    return 0;
}

In addition to using join, you can also use detach()
In C++, std::thread is a class used to create threads, and detach() is a member function of the std::thread class. The detach() function is used to detach a std::thread object from its underlying thread, allowing the thread to run in the background and is no longer associated with the original std::thread object.

When a thread is detached, its lifetime is no longer controlled by the std::thread object, which means that you can no longer join() it or check whether it has Finished. After detaching a thread, the thread's resources will be released automatically after its execution is complete, without the need to explicitly call the join() function.

#include <iostream>
#include <thread>

void threadFunction() {
    
    
    // 假设这是在一个后台线程中执行的函数
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    
    
    std::thread t(threadFunction); // 创建线程对象t,并指定线程函数threadFunction
    t.detach(); // 将线程与线程对象分离,使得线程在后台执行

    // 注意:在此处不能使用t.join(),因为线程已经被分离,没有与t相关联的线程了

    // 主线程继续执行其它任务
    std::cout << "Main thread continues..." << std::endl;

    // 这里可能会发生线程在后台执行的输出,也可能不会,因为线程已经分离了
    // 线程可能在主线程结束前执行,也可能在主线程结束后执行

    return 0;
}

1.1 Usage of std::lock_guard()

std::lock_guardIt is a RAII (resource acquisition is initialization) class template in the C++ standard library, which is used to realize automatic locking and unlocking of mutex ( ) in a multi-threaded environment std::mutex. It provides an easy way to ensure the correct order and timing of acquiring and releasing locks in a code block protected by a mutex, thereby avoiding situations where locks cannot be released due to exceptions or early returns.

std::lock_guardThe usage is as follows:

#include <iostream>
#include <mutex>

std::mutex mtx; // 创建一个互斥锁

void criticalSection() {
    
    
    std::lock_guard<std::mutex> lock(mtx); // 在函数内部创建std::lock_guard对象,并传入互斥锁
    // 在此处放置需要保护的临界区代码
    // 在临界区代码执行期间,互斥锁会被自动上锁
    // 当std::lock_guard对象超出作用域时,会自动释放互斥锁,无需手动解锁
}

int main() {
    
    
    std::thread t1(criticalSection);
    std::thread t2(criticalSection);

    t1.join();
    t2.join();

    return 0;
}

In the example above, std::lock_guard<std::mutex> lock(mtx);this line creates an std::lock_guardobject lockand passes in the mutex mtx. When lock_guardthe object is created, it automatically calls the mutex lock()method to lock it. When lock_guardthe scope of the object ends, whether it is through the normal return of the function, throwing an exception or exiting the scope due to other reasons, it will automatically call the method of the mutex to unlock it, ensuring that the mutex is unlock()locked after the execution of the critical section code is completed. will be released correctly.

The advantage of using std::lock_guardit is that it simplifies the use of mutexes and avoids deadlock problems caused by forgetting to unlock or improper exception handling. It is a relatively safe and recommended way to deal with multi-threaded mutual exclusion access problems.

1.2 Usage of std::atomic_bool

std::atomic_boolIs an atomic Boolean type in the C++ standard library to support atomic operations in multithreaded programming. An atomic operation is an operation that ensures that the operation will not be interrupted by other threads, thereby avoiding the occurrence of race conditions (Race Condition) and data race (Data Race).

What are atomic operations?

Atomic operations (Atomic operations) is an operation in computer science that refers to operations that cannot be interrupted, split, or interleaved. In a multi-threaded or concurrent environment, atomic operations are designed to prevent race conditions (Race Condition) and data races (Data Race). A race condition refers to the non-deterministic behavior of multiple threads accessing a shared resource, and a data race is the undefined behavior caused by multiple threads accessing a memory location.

The atomic operation ensures that the execution of the operation is indivisible, even in a multi-threaded environment, it will not be disturbed by the operations of other threads. Such operations are usually implemented at the hardware level, using specific machine instructions or locking mechanisms.

In programming, atomic operations ensure that concurrent access to shared data is thread-safe. Some common atomic operations include:

  1. Load: Read values ​​from memory to ensure that the read operation is atomic and will not be disturbed by other threads' write operations during the read process.

  2. Store (Store): Write the value into memory, ensuring that the write operation is atomic and will not be disturbed by other threads' read or write operations during the write process.

  3. Exchange (Exchange): Atomic exchange of two values, usually used to implement some synchronization mechanism.

  4. Compare and Swap (CAS): Check whether the value of a memory location is equal to the expected value, and if so, write the new value to the location. This operation is used to implement some locks and synchronization mechanisms.

  5. Increment and Decrement: Atomically increases or decreases the value of a memory location.

In C++, the standard library provides std::atomicclass templates for implementing atomic operations. You can use std::atomicto create variables of atomic type for thread-safe operations in a multi-threaded environment. For example, std::atomic_intto represent an atomic integer type, std::atomic_boolto represent an atomic boolean type, etc.

The atomic boolean type std::atomic_boolsupports the following features:

  1. Atomic operations: std::atomic_bool support various atomic operations, including loading (load), storage (store), exchange (exchange), comparison exchange (compare_exchange_strong and compare_exchange_weak), etc.

  2. Atomic loads and stores: Use loadthe method to atomically get std::atomic_boolthe value of a , and use storethe method to atomically set a new value.

  3. Atomic Swap: Use exchangethe method to atomically exchange std::atomic_boolthe value of and return the previous value.

  4. Compare-Swap: Use compare_exchange_strongthe and compare_exchange_weakmethods to atomically compare the current value with the expected value, and update to the new value if there is a match.

Here's a simple example showing how to use std::atomic_boola thread-safe flag variable:

#include <iostream>
#include <atomic>
#include <thread>

std::atomic_bool flag(false);

void worker() {
    
    
    while (!flag.load(std::memory_order_relaxed)) {
    
    
        // 在这里执行一些工作
    }
    std::cout << "Worker thread finished." << std::endl;
}

int main() {
    
    
    std::thread t(worker);

    // 模拟一些工作
    std::this_thread::sleep_for(std::chrono::seconds(2));

    flag.store(true, std::memory_order_relaxed);

    t.join();

    return 0;
}

In the above example, the main thread sets to, through storethe method , to notify the worker threads to stop working. The worker thread periodically checks the value of through the method to determine whether to continue working. This avoids data race problems that can arise when using standard boolean variables.flagtrueloadflag

In short, std::atomic_boolit is an atomic Boolean type in C++, which is used to perform operations in a multi-threaded environment, ensuring the atomicity of operations to avoid race conditions and data competition.

1.3 Usage of std::condition_variable

When it comes to multi-threaded programming, sometimes it is necessary to make a thread wait until a certain condition is met. std::condition_variableis a mechanism provided in C++ for this purpose.

std::condition_variableUsually std::mutexused with to synchronize the execution of threads and to wait for certain conditions to occur.

The basic concepts are as follows:

  1. One or more threads can wait on a specific condition.
  2. Another thread can be used to notify the waiting thread that the condition has been met.

Main method:

  1. wait(): Makes the thread wait until the condition is met. This is usually used with std::unique_lockand std::mutex.
  2. notify_one(): Wake up a thread waiting for the condition (if one exists).
  3. notify_all(): Wake up all threads waiting for this condition.

Simple example:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    
    
    std::unique_lock<std::mutex> lock(mtx);
    while (!ready) {
    
      // 为防止假唤醒,我们使用一个循环来检查条件
        cv.wait(lock);
    }
    std::cout << "thread " << id << '\n';
}

void go() {
    
    
    std::unique_lock<std::mutex> lock(mtx);
    ready = true;
    cv.notify_all();  // 唤醒所有等待的线程
}

int main() {
    
    
    std::thread threads[10];
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(print_id, i);

    std::cout << "10 threads ready to race...\n";
    go();

    for (auto &th : threads)
        th.join();

    return 0;
}

In the example above we have 10 threads all waiting for the "race" to start. The main thread will set readyto trueand then use cv.notify_all()to notify all waiting threads. In this way, all threads will start executing.

std::condition_variableNote when using :

  • "False wakeups" may occur (i.e. notify_*waiting threads may be woken up without any thread calling). To handle this situation safely, the condition should always be checked in a loop.
  • When using wait()the method, one must be provided std::unique_lock, and the lock should be wait()locked before calling . wait()The lock is automatically released, allowing other threads to enter the critical section, and reacquire the lock after the condition is met.

2. Basic grammar knowledge

2.1 The purpose of static

staticKeywords have different meanings and usages in C++ when applied in different contexts . Here is statica summary of several common uses of :

  1. Static member variable :

    • Static member variables declared in a class are shared by all instances of the class, not independently owned by each instance.
    • Static member variables maintain the same value across all instances of the class.
    • Static member variables can be accessed by class name or instance of the class.
  2. Static member function :

    • Static member functions have nothing to do with the instance of the class and can only access the class's static member variables and other static member functions.
    • Static member functions do not need to be called through an instance of the class when they are called, but can be called directly using the class name.
  3. Static local variable :

    • A static local variable is a variable declared inside a function, but it is only initialized when the function is entered for the first time, and then the last value is maintained when the function exits and enters again.
    • Static local variables maintain state between function calls and can be used to preserve information across multiple function calls.
  4. Static global variable :

    • A static global variable declared outside a function is only visible within the file in which it is declared and will not be affected by other files.
    • Static global variables retain their value throughout the duration of the program and are not restricted by function calls.
  5. Static class members (since C++17):

    • Static members declared in a class can be inlinedesignated as inline with the keyword.
    • Static class members can be initialized directly in the class definition without initialization outside the class.

In summary, staticthere are multiple usages in C++ that can be used to create static members, static functions, static local variables, and limit the scope of global variables. Depending on the context, staticdifferent functions and features are provided.

Among them, static member functions are used in the singleton mode
. In C++, you can define a static member function in a class, and let this static member function return a reference to the class. Here is a sample code to show how to do this:

class MyClass {
    
    
public:
    // 静态公有函数,返回类的引用
    static MyClass& GetInstance() {
    
    
        // 在这里可以进行一些初始化操作,如果需要的话
        
        // 返回类的引用
        static MyClass instance;
        return instance;
    }

    // 其他类成员和函数...

private:
    // 私有构造函数,防止外部直接实例化
    MyClass() {
    
    
        // 构造函数的初始化操作
    }

    // 防止复制和赋值
    MyClass(const MyClass&) = delete;
    MyClass& operator=(const MyClass&) = delete;

    // 私有成员变量和函数...
};

int main() {
    
    
    // 通过静态函数获取类的引用
    MyClass& myInstance = MyClass::GetInstance();

    // 使用myInstance进行操作...

    return 0;
}

In this example, GetInstance()the static member function creates and returns instancea reference to a static local variable, ensuring that there is only one instance of the class and that it is initialized the first time the function is called. Private constructors and deleted copy constructors and assignment operator overloads all help prevent direct instantiation and copying.

3. New features of C++ 11

1.std::function<void()>

std::function<void()>It is a standard library template class introduced in C++11. It is a general function wrapper that can be used to store and call any callable objects, such as functions, function objects, Lambda expressions, etc. Their return types are void, And no parameters.

This template class is defined in <functional>the header file, which can be used to std::function<void()>declare a function wrapper that can store a callable voidobject with a return type of no parameters.

Here are std::function<void()>some important properties and uses of :

  1. Declare the function wrapper:

    #include <functional>
    
    std::function<void()> myFunction;
    

    This declares a named myFunctionthat std::functionwraps voida callable object of return type, with no arguments.

  2. Assignment function object or Lambda expression:

    struct MyFunctor {
          
          
        void operator()() {
          
          
            std::cout << "Hello from functor!" << std::endl;
        }
    };
    
    myFunction = MyFunctor(); // 使用函数对象
    // 或者
    myFunction = []() {
          
          
        std::cout << "Hello from lambda!" << std::endl;
    }; // 使用Lambda表达式
    
  3. Call the function wrapper:

    myFunction(); // 将会调用所包装的可调用对象
    
  4. Check if the function wrapper is empty:

    if (myFunction) {
          
          
        // myFunction 不为空,可以调用
        myFunction();
    }
    
  5. Reset or clear function wrappers:

    myFunction = nullptr; // 或 myFunction = std::function<void()>();
    // 现在 myFunction 变为空,不能再调用
    
  6. Use to std::bindbind a function with parameters:

    #include <iostream>
    #include <functional>
    
    void greet(const std::string& name) {
          
          
        std::cout << "Hello, " << name << "!" << std::endl;
    }
    
    int main() {
          
          
        std::function<void(const std::string&)> greetFunction = std::bind(greet, "Alice");
        greetFunction(); // 输出:Hello, Alice!
        return 0;
    }
    

    Here we bind std::binda parameterized function to , making it a function wrapper with no parameters, but passing the pre-bound parameters when called.greetgreetFunction

In short, std::function<void()>it is a powerful tool that can be used to store and call different types of callable objects at runtime, especially when it is necessary to dynamically select and execute functions according to runtime conditions.

For simple cases, it is more intuitive and convenient to call the functions initA()and directly, and there is no need to use such function objects.initB()std::function<void()>

std::function<void()>It is more suitable for the following scenarios:

  1. Runtime selection of function objects: When you need to select different initialization functions according to conditions or configurations at runtime, you can use std::function<void()>to store and dynamically call the corresponding functions. For example, you can choose to execute different initialization functions based on configuration files or user input without modifying the code logic.

  2. Passing as a parameter: If a function needs to accept a callable object as a parameter, but the specific type of the callable object is uncertain, you can use it std::function<void()>as the parameter type. In this way, the caller can pass any function, function object or Lambda expression, and the callable std::functionobject passed in can be called inside the function through .

  3. As a return value: similarly, if a function needs to return a callable object, but which function or function object to return depends on some conditions, you can use it std::function<void()>as the return type to flexibly return different functions or function objects.

  4. Function pointer replacement: In the case of some legacy codes or interactions with C interfaces, it may be necessary to encapsulate function pointers into safer std::function<void()>objects for ease of use and management.

In summary, std::function<void()>it is more suitable for situations where callable objects need to be dynamically selected or processed at runtime. For fixed, static function calls, it is simpler and clearer to call the function directly. Using std::functionis primarily for greater flexibility and generality.

2. =default(), =delete

In C++, when member functions of a class are declared, we can use = defaultand = deleteto specify their default behavior or to delete the function. These usages are features introduced in C++11.

  1. = default: When we use it after a member function declaration in a class = default, it means that we want the compiler to generate a default function implementation. This is mainly used for special member functions (default constructor, copy constructor, copy assignment operator, and destructor). When we explicitly declare a special member function of a class, the compiler will no longer automatically generate the function. However, if we use it after the function declaration = default, the compiler will automatically generate a default implementation of the function.

    class MyClass {
          
          
    public:
        // 默认构造函数
        MyClass() = default;
    
        // 拷贝构造函数
        MyClass(const MyClass& other) = default;
    
        // 拷贝赋值运算符
        MyClass& operator=(const MyClass& other) = default;
    
        // 默认析构函数
        ~MyClass() = default;
    
        // 其他函数声明
    };
    
  2. = delete: When we use it after a member function declaration of a class = delete, it means that we have disabled the function so that it cannot be called. This is usually used to prevent some inappropriate operations, or to prevent implicit calls of certain functions. Using = deletewill cause the compiler to generate a compilation error when attempting to call the function.

    class MyClass {
          
          
    public:
        // 禁用默认构造函数
        MyClass() = delete;
    
        // 禁用拷贝构造函数
        MyClass(const MyClass& other) = delete;
    
        // 禁用拷贝赋值运算符
        MyClass& operator=(const MyClass& other) = delete;
    
        // 其他函数声明
    };
    

Using = defaultand = deletecan more precisely control the generation and availability of special member functions when designing the interface of a class, thereby increasing the readability and security of the code.

3.std::enable_shared_from_this

In C++, std::enable_shared_from_thisit is a template class that provides a way to safely obtain a reference to it in an object that inherits the class std::shared_ptr. This is very useful when dealing with resource management and object lifecycle control based on shared pointers, especially when there are mutual references between objects.

This template class is often std::shared_ptrused with to ensure that the reference count of the shared pointer is properly managed until the object is deleted.

Here are std::enable_shared_from_thisthe basic usage and examples of :

#include <iostream>
#include <memory>

class MyClass : public std::enable_shared_from_this<MyClass> {
    
    
public:
    std::shared_ptr<MyClass> getShared() {
    
    
        return shared_from_this();  // 获取指向当前对象的 shared_ptr
    }

    void print() {
    
    
        std::cout << "Hello from MyClass" << std::endl;
    }
};

int main() {
    
    
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    std::shared_ptr<MyClass> ptr2 = ptr1->getShared();

    ptr1->print();
    ptr2->print();

    return 0;
}

In this example, MyClassinheriting from std::enable_shared_from_this<MyClass>, which allows MyClassthe object to internally call shared_from_this()the method to get a pointer to itself std::shared_ptr. In this way, the shared pointer of the object can be obtained without increasing the reference count, avoiding memory leaks caused by circular references.

Note the following points:

  1. must be used std::shared_ptrto manage the object, otherwise shared_from_this()undefined behavior results.
  2. Objects must std::shared_ptrbe shared through , not managed through raw pointers or other smart pointers.
  3. It must only be called once during the object's lifetime shared_from_this(), otherwise undefined behavior results.

In short, std::enable_shared_from_thisit provides a safer and more convenient method for resource management of shared pointers, especially for situations involving circular references.

Guess you like

Origin blog.csdn.net/qq_44660367/article/details/132078316