【问题标题】:How should I return an object from a function?我应该如何从函数中返回一个对象?
【发布时间】:2017-08-08 21:39:56
【问题描述】:

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

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 应该能够授予用户访问枚举的COutputs 的权限。

实现这一点的第一种方法是返回一个指针:

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

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

第二种方法是通过引用返回:

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

这里的问题与使用指针的方法相同。此外,它还有一个额外的警告,即不能返回真正的无效对象。如果将nullptr 作为返回指针返回,则很明显它是“无效的”。但是,在引用方面,没有与 nullptr 等效的选项。

继续接近第三名。按值返回。

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

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

我可以继续...

例如,COutput 对象可以在堆上分配并存储在std::shared_ptrs 中,然后按原样返回。但是,这会使代码非常冗长。

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

【问题讨论】:

  • 视具体情况而定。不同的情况有不同的退货方式。我认为您需要更具体。
  • 通过引用/值返回的两种可能的解决方案:1) 对超出范围的索引抛出异常(这是std::vector::at 所做的),或 2) 对超出范围的索引有未定义的行为(这就是std::vector::operator[] 所做的)。
  • @Frxstrem 但这仍然不能解决必须复制对象的问题
  • 为什么复制对象有问题?
  • 如果您想返回一个对象而不复制它或放弃所有权,请考虑std::weak_ptr

标签: c++ pointers reference return-by-value


【解决方案1】:

首先让我说,你绝对不应该开始搞乱shared_ptr 来解决这个问题。只是不要这样做。这里有几个不同的选择是合理的。

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

其次,您可以通过指针/引用返回。是的,它可以悬挂。但是 C++ 并没有声称可以提供针对这种情况的保护。每个标准容器都具有 begin()end() 方法,它们返回迭代器也很容易悬空。期望客户避免这些陷阱在 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();
});

在这种情况下,用户需要更加努力地使某些东西悬空(尽管仍然有可能),因为他们没有得到指针/引用,而且您也不必制作副本。

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      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

      【讨论】:

        猜你喜欢
        • 2016-12-13
        • 2017-09-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-04-29
        • 2015-12-15
        • 1970-01-01
        • 2022-01-03
        相关资源
        最近更新 更多