如何在C ++中创建一个无需复制即可返回静态或自动存储对象的函数?

时间:2018-11-14 16:24:38

标签: c++

假设我要编写如下函数:

  • 它返回一个对象
  • 在某些情况下,根据函数的参数,对象具有固定值,该值只能计算一次以节省时间。因此,自然的选择是使该对象static
  • 否则,该函数必须动态生成对象

编写此函数的最佳条件是:

  • 调用时不需要复制构造函数,只需移动即可,以防止复制昂贵对象的所有数据
  • 我不想使用new原始指针。如果需要指针,它们必须是聪明的并且可以自动删除

用例如下:

  • 某些值很常见,所以我要缓存它们
  • 其他值非常罕见,所以我希望不要将其缓存

调用者不应该知道哪些值是通用的,两种情况下的接口都应该透明。

到目前为止,我管理过的唯一干净的实现是使用shared_ptr,如下所示,但是感觉有点过头了。特别是,由于它进行堆分配,因此感觉上并不需要。有更好的方法吗?

#include <cassert>
#include <iostream>
#include <memory>

struct C {
    int i;
    static int count;
    C(int i) : i(i) {
        std::cout << "constr" << std::endl;
        count++;
    }
    C(const C& c) : C(c.i) {
        std::cout << "copy" << std::endl;
    }
    ~C() {
        std::cout << "destr" << std::endl;
        count--;
    }
};
int C::count = 0;

std::shared_ptr<C> func_reg_maybe_static(int i) {
    static auto static_obj = std::make_shared<C>(0);
    if (i == 0) {
        return static_obj;
    } else {
        return std::make_shared<C>(i);
    }
}

int main() {
    assert(C::count == 0);

    {
        auto c(func_reg_maybe_static(0));
        assert(c->i == 0);
        assert(C::count == 1);
    }
    assert(C::count == 1);

    {
        auto c(func_reg_maybe_static(0));
        assert(c->i == 0);
        assert(C::count == 1);
    }
    assert(C::count == 1);

    {
        auto c(func_reg_maybe_static(1));
        assert(c->i == 1);
        assert(C::count == 2);
    }
    assert(C::count == 1);

    {
        auto c(func_reg_maybe_static(2));
        assert(c->i == 2);
        assert(C::count == 2);
    }
    assert(C::count == 1);
}

我使用GCC 6.4.0进行编译:

g++ -std=c++17 -Wall -Wextra -pedantic-errors -o main.out func_ret_maybe_static.cpp

并产生预期的输出(我相信通过复制省略来保证):

constr
constr
destr
constr
destr
destr

我添加了静态引用计数器C::count只是为了检查对象是否确实按预期被删除。

如果没有静态大小写,我将直接执行以下操作:

C func_reg_maybe_static(int i) {
    return C(i);
}

并复制省略/移动语义将使一切高效。

但是,如果我尝试类似的操作:

C func_reg_maybe_static(int i) {
    static C c(0);
    return c;
}

然后,C ++会聪明地停止移动C,并开始复制它,以避免损坏static

3 个答案:

答案 0 :(得分:6)

我不太了解静态/自动内容的用途。如果您想做的就是避免在以前使用该参数时重复构造,为什么不只拥有一个缓存呢?

C& func_reg(const int i)
{
    static std::unordered_map<int, C> cache;
    auto it = cache.find(i);
    if (it == cache.end())
      it = cache.emplace(i, C(i));

    return it->second;
}

不是总是想要缓存吗?精细!添加此内容:

C func_reg_nocache(const int i)
{
    return C(i);
}

如果我误解了,而您真的需要这个东西,那么传递is_static == true会给您一个完全不同的对象(一个由0而不是i构造的对象),那么只需创建一个新函数为了那个原因;它正在做一些与众不同的事情。

C& func_reg()
{
    static C obj(0);
    return obj;
}

C func_reg(const int i)
{
    return C(i);
}

答案 1 :(得分:4)

根据不透明的条件,返回的对象可能引用了先前存在的对象。这意味着该函数需要通过引用返回。但是由于未缓存值的存储空间必须超过该函数并且不能在堆上,因此调用方需要抢先为返回的对象在堆栈上提供存储空间。

C& func_reg_maybe_static(int i, void* buf)
{
    static C c(0);
    if(i == 0)
        return c;
    else
        return *new (buf) C(i);
}

using uninit_C = std::aligned_storage<sizeof(C), alignof(C)>;
uninit_C buf;
auto& c = func_reg_maybe_static(i, &buf);

当且仅当析构函数具有副作用时,这才需要在下一行进行手动析构函数调用。另一种选择是

C& func_reg_maybe_static(int i, C& buf)
{
    static C c(0);
    if(i == 0)
        return c;
    else
    {
        buf.~C();
        return *new (&buf) C(i); // note below
    }
}

C buf;
auto& c = func_reg_maybe_static(i, buf);

此版本显然要求C是默认可构造的。 new放置完全合法,但是根据buf的内容,在函数调用may or may not be legal之后使用C。析构函数将在范围出口处调用。

答案 2 :(得分:3)

您想要的基本上是一个变体:

std::variant<C, C*> func_reg_maybe_static(int i)
{
  static C static_obj{ 0 };
  if (i == 0) {
    return &static_obj;
  } else {
    return std::variant<C, C*>{ std::inplace_type_t<C>{}, i };
  }
}

其优点是不需要任何其他内存分配(与shared_ptr方法相比)。但是在调用方使用它有点不方便。我们可以通过编写包装器类来解决此问题:

template <class C> class value_or_ptr
{
  std::variant<C, C*> object_;
public:
  explicit template <class... Params> value_or_ptr(Params&&... parameters)
    : object_(std::inplace_type_t<C>{}, std::forward<Params>(parameters)...)
  {}
  explicit value_or_ptr(C* object)
    : object_(object)
  {}
  // other constructors...

  C& operator*()
  {
    return object_.index() == 0 ? std::get<0>(object_) : *std::get<1>(object_);
  }

  // other accessors...
}

如果您没有C ++ 17编译器,则可以用联合来完成同样的工作,但是实现当然会更复杂。原始功能将变为:

value_or_ptr<C> func_reg_maybe_static(int i)
{
  static C static_obj{ 0 };
  if (i == 0) {
    return value_or_ptr{ &static_obj };
  } else {
    return value_or_ptr{ i };
  }
}

与超载对比

我们可以通过函数重载获得类似的结果:

C& func_reg()
{
  static C obj{ 0 };
  return obj;
}

C func_reg(const int i)
{
  return C{ i };
}

这样做的优点是更简单,但可能要求调用者进行复制。它还要求调用者知道是否将返回预先计算的对象(这是不希望的)。使用变体方法,呼叫者可以统一对待结果。调用方始终会获取具有值语义的对象,而无需知道他是否将收到预先计算的对象。