函数参数的 C++ 所有权

C++ ownership of a function parameter

我写了一个具有这种形式的函数:

Result f( const IParameter& p);

我的意图是这个签名将明确表示函数没有取得参数 p.

的所有权

问题是 Result 将保留对 IParameter 的引用:

class Result
{  
    const IParameter& m_p;

public: 
    Result( const IParameter& p ) 
        : m_p( p ){ } 
};

但后来碰巧有人这样调用函数:

const auto r = f(ConcreteParameter{});

不幸的是,临时文件可以绑定到 const 引用,这导致了崩溃。

问题是:我怎样才能明确该函数不应该用临时对象调用,并且当发生这种情况时可能会出现一个很好的编译错误?在这种情况下声明它没有取得所有权实际上是错误的,因为将它传递给将在函数调用范围之外传播的结果吗?

最简单的表达方式是使用右值引用参数重载函数。对于临时对象,它们比 const 引用更受欢迎,因此将选择它们。如果您随后删除上述重载,您将得到一个不错的编译器错误。对于您的代码,它看起来像:

Result f( const IParameter&& ) = delete;

你也可以用 Result 做同样的事情来保护它,看起来像:

class Result
{  
    const IParameter& m_p;

public: 
    Result( const IParameter& p ) 
        : m_p( p ){ } 
    Result( const IParameter&& ) = delete;
};

您可以从重载集中手动删除接受 IParameter&& 右值的构造函数:

class Result
{
    // ...

    public:
    Result( IParameter&& ) = delete; // No temporaries!
    Result( const IParameter& p );
};

当客户端代码尝试通过

实例化一个对象时
Result f(ConcreteParameter{}); // Error

由于缺少 const-ness,采用 const 限定引用的构造函数不匹配,但右值构造函数完全匹配。由于这个是 = deleted,编译器拒绝接受这样的对象创建。

请注意,正如评论中所指出的,这可以通过 const 合格的临时对象来规避,请参阅 以了解如何确保这种情况不会发生。

另请注意,并非所有人都同意这是一种好的做法,例如,请查看 here(位于 15:20)。

很难说。最好的方法是记录 Result 的寿命不得超过用于构造它的 IParameter

存在作为完全有效的构造函数发送的临时对象的有效案例。想想这个:

doSomethingWithResult(Result{SomeParameterType{}});

删除临时构造函数会阻止此类有效代码。

此外,删除右值构造函数并不能阻止所有情况。想想这个:

auto make_result() -> Result {
    SomeParameterType param;
    return Result{param};
}

即使删除临时构造函数,还是很容易产生无效代码。无论如何,您 必须记录参数的生命周期要求。

因此,如果您无论如何都必须记录此类行为,我会选择标准库对 string views:

的处理
int main() {
    auto sv = std::string_view{std::string{"ub"}};
    std::cout << "This is " << sv;
}

它不会阻止从临时字符串构造字符串视图,因为它很有用,就像我的第一个示例一样。

一般来说,如果函数通过 const& 接收值,则函数会使用该值,但不会保留它。您确实持有对值的引用,因此您应该更改参数类型以使用 shared_ptr(如果资源是强制性的)或 weak_ptr(如果资源是可选的)。否则你会 运行 不时遇到这类问题,因为没有人阅读文档。

我已经将@NathanOliver 的答案投票选为最佳答案,因为我真的认为我提供的信息已经给出了它。另一方面,当函数比我最初的示例中的函数更复杂时,我想分享我认为更好的解决方案来解决这个非常具体的场景。

delete 解决方案的问题是它随着参数的数量呈指数增长,假设所有参数都需要在函数调用结束后保持活动状态并且您希望编译时检查用户您的 API 并未尝试将这些参数的所有权赋予函数:

void f(const A& a, const B& b)
{
    // function body here
}

void f(const A& a, B&& b) = delete;
void f(A&& a, const B& b) = delete;
void f(A&& a, B&& b) = delete;

我们需要delete所有可能的组合,这将很难在长运行上维护。所以我提出的解决方案是利用这样一个事实,即通过移动包装 Treference_wrapper 构造函数已经在 STD 中删除,然后这样写:

using AConstRef = reference_wrapper<const A>;
using BConstRef = reference_wrapper<const B>;

void f(AConstRef a, BConstRef b)
{
    // function body here
}

这样所有无效的重载都会被自动删除。到目前为止,我没有看到这种方法有任何缺点。