C++函数参数的所有权

C++ ownership of a function parameter

本文关键字:所有权 参数 函数 C++      更新时间:2023-10-16

我写了一个具有以下形式的函数:

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{});

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

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

明确这一点的最简单方法是使用右值引用参数重载函数。 这些优先于临时的常量引用,因此将选择它们。 如果你随后删除上述重载,你会得到一个很好的编译器错误。 对于您的代码,如下所示:

Result f( const IParameter&& ) = delete;

您也可以对Result执行相同的操作来保护它,如下所示:

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

通常,如果函数通过const&接收值,则预计该函数将使用该值,但不会保存它。您确实持有对值的引用,因此您可能应该将参数类型更改为使用shared_ptr(如果资源是必需的)或weak_ptr(如果资源是可选的)。否则,你会不时遇到这种问题,因为没有人阅读文档。

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

有一些有效的临时函数作为构造函数发送的情况是完全有效的。想一想:

doSomethingWithResult(Result{SomeParameterType{}});

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

此外,删除右值构造函数不会阻止所有情况。想一想:

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

即使删除了带有临时的构造函数,无效代码仍然很容易制作。无论如何,您都必须记录参数的生存期要求。

因此,如果您无论如何都必须记录此类行为,我会选择标准库对字符串视图的作用:

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

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

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

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

当客户端代码尝试通过以下方式实例化对象时

Result f(ConcreteParameter{}); // Error

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

请注意,正如评论中指出的那样,这可以通过const合格的临时人员来规避,请参阅@NathanOliver的答案,了解如何确保这种情况不会发生。

另请注意,并非每个人都同意这是好的做法,例如,请看这里(15:20)。

我已经投票@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
}

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