【问题标题】:Lifetime extension of temporaries' data members and API design临时数据成员的生命周期延长和API设计
【发布时间】:2016-12-31 21:03:12
【问题描述】:

假设我有一个跨平台的Path 类,例如:

class Path {
public:
    // ...
    Path parent() const;                // e.g., /foo/bar -> /foo

    std::string const& as_utf8() const {
        return path;
    }
private:
    std::string path;
};

parent() 成员函数返回 this 路径的父路径,因此它(正确地)返回一个新构造的 Path 对象来表示它。

对于将操作系统级别的路径表示为 UTF-8 字符串的平台(例如,Unix),as_utf8() 直接返回对内部表示 path 的引用似乎是合理的,因为它已经 UTF-8。

如果我有这样的代码:

std::string const &s = my_path.as_utf8();  // OK (as long as my_path exists)
// ...
Path const &parent = my_path.parent();     // OK (temporary lifetime extended)

这两行都很好,因为:

  • 假设my_path 持续存在,那么s 仍然有效。
  • parent() 返回的临时对象的生命周期由const& 延长。

到目前为止,一切都很好。但是,如果我有这样的代码:

std::string const &s = my_path.parent().as_utf8(); // WRONG

那么这是错误,因为parent() 返回的临时对象确实没有延长其生命周期,因为const& 确实没有指的是临时的,但指的是它的数据成员。此时,如果您尝试使用s,您将得到垃圾或核心转储。如果代码是:

    std::string as_utf8() const {                 // Note: object and NOT const&
        return path;
    }

那么代码就是正确的。但是,每次调用此成员函数时都创建一个临时的会是低效的。这也意味着没有“getter”成员函数应该永远返回对其数据成员的引用。

如果 API 保持原样,那么调用者必须查看 as_utf8() 的返回类型以查看它是否返回 const& 似乎会给调用者带来过度的负担:如果它确实如此,那么调用者必须使用一个对象并且不能使用const&;如果它返回一个对象,那么调用者可以使用const&

那么有什么办法可以解决这个问题,让 API 在大多数情况下既高效又能防止用户从看似无害的代码中获取悬空引用?


顺便说一句,这是使用 g++ 5.3 编译的。临时应该的生命周期可能会延长,但是编译器有错误。

【问题讨论】:

  • 那么这是错误的,因为 parent() 返回的临时对象没有延长其生命周期,因为 const& 不是指临时对象,而是它的数据成员这是错误的。我正在寻找显示寿命延长的问题
  • @NathanOliver 但是temp().membertemp().get() 之间存在区别,get() 恰好返回member
  • @NathanOliver:编译器怎么可能知道get返回一个成员的引用?编译器只能看到get 返回一个reference。不,临时生命周期延长仅在您获得 NSDM 时发生,而不是调用成员函数。
  • @NathanOliver 如果get 不是内联的,编译器如何知道延长临时的生命周期?
  • 好的,只有当你直接引用成员时,编译器才被允许延长生命周期。谢谢。

标签: c++ api-design temporary data-members


【解决方案1】:

您可以做的是创建两个不同版本的as_utf8(),一个用于左值,一个用于右值。不过你需要 C++11。

这样,您可以两全其美:const&(当对象不是临时对象时)和有效移动(当对象不是临时对象时):

std::string const& as_utf8() const & {
                               // ^^^ Called from lvalues only
    return path;
}

std::string as_utf8() const && {
                        // ^^^^ Called from rvalues only
    return std::move(path); //We don't need path any more
}

【讨论】:

  • 如果您必须支持的平台之一没有 C++11 编译器,您会怎么做?
  • @PaulJ.Lucas 好吧,这取决于。我会使用另一个平台来编译我必须支持的平台的代码:) 如果这不可能,我会使用第二个版本。鉴于path 在大多数情况下并不长,我认为性能损失并不重要,因为它是最小的:)
  • @PaulJ.Lucas:我不会再担心这种琐碎的效率问题,只需按值返回std::string。实际上,除非 OP 有证据表明这些副本很昂贵,否则我无论如何都会这样做。
  • 我会移动 path 以获得右值以避免复制。
  • @Jarod42 这不是“阻止”编译器使用 RVO 吗?
【解决方案2】:

在我看来,返回引用还是返回对象的指导原则是检查原始类的已定义角色。

即是公开简单属性的方法(争论引用,特别是如果它是不可变的),还是生成什么?

如果它正在生成一个新的对象或表示,我们可以合理地期望它返回一个不同的对象。

API 的用户通常习惯于理解属性不会超过其宿主对象的寿命。这当然可以在文档中明确说明。

例如

struct path
{
    /// a property
    /// @note lifetime is no longer than the lifetime of this object
    std::string const& native() const;

    /// generate a new string representation in a different format
    std::string to_url() const;

};

在这种情况下,我个人会避免使用 as_ 前缀,因为对我来说,这表明我们正在返回同一对象的新表示,例如:

struct world 
: std::enable_shared_from_this<world>
{
    struct sky {} my_sky_;

    /// returns a shared_ptr to my sky object, which shares its lifetime
    /// with this world.
    std::shared_ptr<sky> as_sky() 
    { 
      return std::shared_ptr<sky>(shared_from_this(), std::addressof(my_sky_));
    }
};

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-05-20
    • 2013-11-20
    • 2019-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多