我应该如何 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" 之前,不会调用基对象的析构函数。在处理对单个对象的多个引用时,这消除了很多(但不是全部)令人头疼的问题。
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用于这种场景。
考虑以下场景:有一个 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" 之前,不会调用基对象的析构函数。在处理对单个对象的多个引用时,这消除了很多(但不是全部)令人头疼的问题。
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用于这种场景。