假设我要编写如下函数:
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
。
答案 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 };
}
这样做的优点是更简单,但可能要求调用者进行复制。它还要求调用者知道是否将返回预先计算的对象(这是不希望的)。使用变体方法,呼叫者可以统一对待结果。调用方始终会获取具有值语义的对象,而无需知道他是否将收到预先计算的对象。