[Effective Modern C++] 条款18笔记

条款18中的完整代码:

点击查看代码
#include <iostream>
#include <memory>
#include <string>

// 假设基础的 Investment 类和 Stock 类
class Investment {
public:
    virtual ~Investment() = default;
    virtual void display() const = 0;
};
// 其它类类似,略
class Stock : public Investment {
    std::string name;
public:
    Stock(const std::string& stockName) : name(stockName) {}
    void display() const override {
        std::cout << "Stock: " << name << std::endl;
    }
};

void makeLogEntry(Investment* pInvestment) {
    std::cout << "Logging investment action" << std::endl;
}

template<typename... Ts>
auto makeInvestment(Ts&&... params) 
{
    // 自定义删除器
    auto delInvmt = [](Investment* pInvestment) 
    {
        makeLogEntry(pInvestment);
        delete pInvestment;
    };

    // 使用 std::unique_ptr 来管理 Investment 对象,指定 delInvmt 作为自定义删除器
    std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);

    // 根据条件创建 Investment 对象
	if ( /* a Stock object should be created */ )
	{
		pInv.reset(new Stock(std::forward<Ts>(params)...));
	}
	else if ( /* a Bond object should be created */ )
	{
		pInv.reset(new Bond(std::forward<Ts>(params)...));
	}
	else if ( /* a RealEstate object should be created */ )
	{
		pInv.reset(new RealEstate(std::forward<Ts>(params)...));
	}

    return pInv; // 返回 unique_ptr,发生移动构造
}

int main() {
    auto investment = makeInvestment("TechCorp");
    if (investment) {
        investment->display();
    }
    return 0;
}

针对上述代码有两点疑问:

首先,为什么params需要使用std::forward,而不能直接赋值;其次,return pInv;返回unique_ptr是否发生了复制构造操作?

这两个问题涉及到 C++11/14 中的完美转发(perfect forwarding)和智能指针的行为。我会详细解释为什么在 makeInvestment 中使用 std::forward,以及 std::unique_ptr 的返回过程是否会发生复制构造操作。

1. 为什么 params 需要使用 std::forward,而不能直接赋值?

在 C++ 中,函数模板和可变参数模板允许我们编写接受任意类型和数量参数的函数。为了正确处理这些参数的值类别(lvalue/rvalue),我们需要使用 std::forward

完美转发(Perfect Forwarding)

  • 完美转发 是指在函数模板中保留参数的原始值类别(是左值还是右值),以便在调用其他函数时保持参数的最佳传递方式。
  • 使用 std::forward 可以将参数以其原始的值类别传递给另一个函数。例如,如果一个参数在调用时是右值(临时对象),我们希望它继续作为右值传递下去,以便利用移动语义来提高性能;而如果它是左值,我们希望它继续作为左值传递。

示例

假设 makeInvestment 函数模板传递了一个右值参数,而我们希望在创建 Stock 对象时,该右值参数能利用移动构造函数进行高效的对象创建。如果我们不使用 std::forward,将直接使用参数构造 Stock 对象,可能会导致多余的拷贝操作,无法充分利用 C++11 提供的移动语义

具体原因:

template<typename... Ts>
auto makeInvestment(Ts&&... params) 
{
    if ( /* 条件 */ ) 
    {
        // 创建 Stock 对象时,使用 std::forward 保持参数的原始值类别
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }

    return pInv;
}
  • 没有 std::forward 的情况:

    • 如果我们直接使用 params... 传递给 Stock 的构造函数:
      pInv.reset(new Stock(params...));
      
    • 这种情况下,所有的参数都会被视为左值(即使它们最初是右值)。这可能导致不必要的拷贝构造,而不是移动构造,特别是在处理右值时。
  • std::forward 的情况:

    • std::forward<Ts>(params)... 能确保每个参数都以其原始的值类别传递(右值保持右值,左值保持左值)。
    • 对于右值参数,这意味着它们会利用移动构造或移动赋值运算符,这通常是更高效的。

总结来说,std::forward 确保了参数在转发时能够保持它们的值类别,从而避免不必要的拷贝,提高了效率和性能,特别是在处理临时对象和大数据结构时。

2. return pInv 是否会发生 unique_ptr 的复制构造操作?

std::unique_ptr 是一个独占所有权的智能指针,它不支持复制构造,但支持移动构造。这意味着 std::unique_ptr 不能被复制,但可以被移动。

返回 std::unique_ptr 时的行为:

return pInv;
  • 移动语义(Move Semantics):
    • std::unique_ptr 被返回时,编译器会自动使用移动构造函数来将所有权转移到返回的 std::unique_ptr 中。
    • 由于 std::unique_ptr 不支持复制构造,所以编译器不会尝试进行复制,而是使用移动构造来转移资源。
    • 这符合 C++11/14 的移动语义设计,使得返回 std::unique_ptr 高效且安全。

详细解释:

当我们在函数中返回一个 std::unique_ptr 时,如 return pInv;

  • 没有移动构造的显式调用

    • 编译器会尝试自动优化,通常是通过移动构造将返回的对象转移到调用者。
    • 这意味着函数返回时,pInv 所指向的资源的所有权会被转移到接收返回值的对象中,而 pInv 自身将不再持有该资源。
  • 返回值优化(RVO, Return Value Optimization)

    • 在很多情况下,编译器会进行返回值优化,直接构造返回的 unique_ptr 对象,而不是先在函数内部创建,然后再移动到外部。
    • 这种优化消除了不必要的对象构造和析构,提高了性能。

所以,在返回 std::unique_ptr 时,不会进行复制构造,而是使用移动构造或通过返回值优化直接生成最终的对象,保证了效率和资源管理的安全性。

在这个示例中:

  • std::forward 保证了参数 params 被以正确的值类别传递给 Stock 的构造函数。
  • return pInv; 返回时使用了 std::unique_ptr 的移动语义,确保资源的高效管理和转移。
posted @ 2024-06-21 16:58  围城chen  阅读(1)  评论(0编辑  收藏  举报