我应该如何 return 来自函数的对象?

How should I return an object from a function?

考虑以下场景:有一个 class CDriver 负责枚举所有连接的输出设备(由 COutput class 表示)。其代码可能如下所示:

class COutput
{
    // COutput stuff
};

class CDriver
{
public:
    CDriver();  // enumerate outputs and store in m_outputs

    // some other methods

private:
    std::vector<COutput> m_outputs;
};

现在 CDriver 应该能够授予用户对枚举的 COutput 的访问权限。

实现这个的第一个方法是return一个指针:

const COutput* GetOutput(unsigned int idx) const 
{ 
    return idx < m_outputs.size() ? &m_outputs[idx] : nullptr; 
}

在我看来,此方法存在的问题是,如果指针由用户存储并且在 CDriver 对象被销毁后它仍然存在,它现在是一个悬空指针。这是因为指针对象(COutput 对象)在 CDriver 对象的析构函数中被销毁了。

第二种方法是 return 参考:

const COutput& GetOutput(unsigned int idx) const 
{ 
    return idx < m_outputs.size() ? &m_outputs[idx] : m_invalidOutput; 
}

这里存在与指针方法相同的问题。此外,它还有一个额外的警告,即不能 returned 真正的无效对象。如果一个 nullptr 被 return 编辑为一个 return 指针,很明显它是 "invalid"。但是,在引用方面,没有等同于 nullptr 的东西。

继续接近第三点。 Return 按值。

COutput GetOutput(unsigned int idx) const 
{ 
    return idx < m_outputs.size() ? &m_outputs[idx] : m_invalidOutput; 
}

在这里,用户不必担心 returned 对象的生命周期。但是,必须复制 COutput 对象,并且与引用方法类似,没有直观的方法来检查错误。

我可以继续...

例如,COutput 对象可以在堆上分配并存储在 std::shared_ptr 中,然后 return 就这样编辑。但是,这会使代码非常冗长。

有没有什么方法可以直观地解决这个问题,并且不会引入不必要的代码冗长?

首先我要说的是,你绝对不应该为了解决这个问题而开始胡思乱想shared_ptr。只是不要这样做。你在这里有几个不同的选择是合理的。

首先,您可以简单地 return 按值。如果 COutput 很小,这是一个很好的方法。要处理越界索引,您有两种选择。一种是抛出异常。效果很好,很容易。这是我最有可能在这里推荐的。确保有一个 size() 成员,用户可以调用该成员来获取大小,这样他们就可以避免支付抛出的成本,如果这对他们来说太贵了。您还可以 return 和 optional。这在 17 的标准库中,在 boost prior 中,并且有独立的实现。

其次,您可以通过pointer/referencereturn。是的,它可以悬挂。但是 C++ 并没有声称对此提供保护。每个标准容器都具有 begin()end() 方法,return 迭代器也可以轻松悬挂这些方法。期望客户端避免这些陷阱在 C++ 中并非不合理(尽管你当然应该记录它们)。

第三,你可以做控制反转:不是给用户一个对象来操作,而是让用户传递他们想要执行的操作。换句话说:

template <class F>
auto apply(std::size_t idx, F f) const 
{
    if (idx >= m_outputs.size())
    {
        throw std::out_of_range("index is out of range");
    }

    return f(m_outputs[idx]); 
}

用法:

CDriver x;
x.apply(3, [] (const COutput& o) {
    o.do_something();
});

在这种情况下,用户需要更加努力地制作一些东西(尽管仍然有可能),因为他们没有得到 pointer/reference,而且您不必制作副本要么。

您当然可以通过多种方式更改apply;例如不是从函数调用返回 return 而是 return true/false 来指示索引是否在范围内而不是抛出。基本思想是一样的。请注意,必须修改此方法才能与虚函数结合使用,这会降低它的可取性。因此,如果您正在考虑 CDriver 的多态性,您应该考虑一下。

看看 C++11 的共享指针。使用共享指针,在销毁该对象的所有共享指针 "owning" 之前,不会调用基对象的析构函数。在处理对单个对象的多个引用时,这消除了很多(但不是全部)令人头疼的问题。

这里有更多信息: http://en.cppreference.com/w/cpp/memory/shared_ptr

1).久经考验:抛出一个标准argument exception

2) 您可以使用元组和 std::tie

const std::tuple<bool, COutput> GetOutput(unsigned int idx) const 
{ 
     return idx < m_outputs.size() 
               ? std::make_tuple(true m_outputs[idx])
               : std::make_tuple(false,  m_invalidOutput); 
}

bool has_value;
COutput output;

std::tie(has_value, output) = GetOutput(3);

要替换元组,可以使用C++17 structured bindings中的std::tie。

3) C++17 将std::optional用于这种场景。