(可能与How to implement a C++ method that creates a new object, and returns a reference to it有关,它与某些内容有所不同,但在内容中包含几乎完全相同的代码)
我想从静态函数返回对静态局部的引用。当然,我可以让它工作,但它不如我喜欢的那么漂亮。
可以改进吗?
除了以明确定义的方式可靠地获取或初始化资源并释放它之外,我还有几个不做太多的课程。他们甚至不需要对资源本身了解太多,但用户可能仍希望以某种方式查询某些信息。
这当然很简单:
struct foo { foo() { /* acquire */ } ~foo(){ /* release */ } };
int main()
{
foo foo_object;
// do stuff
}
微不足道的。或者,这也可以正常工作:
#include <scopeguard.h>
int main
{
auto g = make_guard([](){ /* blah */}, [](){ /* un-blah */ });
}
除了现在,查询东西有点困难,而且它不如我喜欢的那么漂亮。如果你更喜欢Stroustrup而不是Alexandrescu,你可以改为使用GSL并使用一些涉及final_act
的混合物。不管。
理想情况下,我想写一些类似的东西:
int main()
{
auto blah = foo::init();
}
如果您希望这样做,您可以获取对象的引用。或者忽略它,或者其他什么。我的直接想法是:简单,这只是梅耶的伪装单身人士。因此:
struct foo
{
//...
static bar& init() { static bar b; return b; }
};
那就是它!死简单,完美。当您致电foo
时会创建init
,您可以获得可以查询统计信息的bar
,并且它是参考资料,因此您不是所有者,foo
{1}}自动清理结束。
...除
当然不能这么容易,任何使用基于范围的for auto
的人都知道你必须写auto&
如果你不想要惊喜副本。但是,唉,auto
看起来非常无辜,我没有想到它。另外,我明确地返回了一个引用,那么auto
可能捕获的只是一个引用!
结果:制作了一份副本(大概来自返回的参考文献?)当然有一个范围的生命周期。调用默认复制构造函数(无害,无效),最终复制超出范围,上下文在操作中释放,内容停止工作。在程序结束时,再次调用析构函数。 Kaboooom。嗯,那是怎么发生的。
显而易见(好吧,在第一秒中不那么明显!)解决方案是写:
auto& blah = foo::init();
这很有效,而且运行正常。问题解决了,除了......除非它不漂亮,人们可能会意外地像我一样做错了。我们不需要额外的&符号吗?
它可能也可以返回shared_ptr
,但这将涉及不必要的动态内存分配,更糟糕的是,它将是错误的&#34;在我看来。您不分享所有权,您只能被允许查看其他人拥有的内容。原始指针?正确的语义,但......呃。
通过删除复制构造函数,我可以防止无辜的用户遇到忘记的&amp;陷阱(这将导致编译器错误)。
然而,这仍然不如我想的那么漂亮。必须有一种沟通方式&#34;这个返回值将作为参考&#34; 到编译器?像return std::as_reference(b);
?这样的东西
我曾想过一些涉及&#34;移动&#34;没有真正移动它的对象,但不仅仅是编译器几乎肯定不会让你移动一个静态本地,但如果你设法做到这一点,你要么改变了所有权,要么用'#34;假动作&#34 ; move-constructor再次调用析构函数两次。所以这没有解决方案。
是否有更好,更漂亮的方式,或者我只需要写auto&
?
答案 0 :(得分:5)
返回std :: as_reference(b);?
之类的东西
你的意思是std::ref
?这将返回您提供的值的std::reference_wrapper<T>
。
static std::reference_wrapper<bar> init() { static bar b; return std::ref(b); }
当然,auto
会将返回的类型推断为reference_wrapper<T>
而不是T&
。虽然reference_wrapper<T>
具有隐式operatorT&
,但这并不意味着用户可以像引用一样使用它。要访问成员,他们必须使用->
或.get()
。
然而,无论如何,我相信你的想法是错误的。原因是auto
和auto&
是每个C ++程序员需要学习如何处理的东西。人们不会让他们的迭代器类型返回reference_wrapper
而不是T&
。人们通常不会以这种方式使用reference_wrapper
。
因此,即使您像这样包装所有接口,用户仍然必须最终知道何时使用auto&
。实际上,除了特定的API之外,用户还没有获得任何安全性。
答案 1 :(得分:5)
强制用户通过引用捕获是一个三步过程。
首先,使返回的东西不可复制:
struct bar {
bar() = default;
bar(bar const&) = delete;
bar& operator=(bar const&) = delete;
};
然后创建一个小的直通功能,可靠地提供引用:
namespace notstd
{
template<class T>
decltype(auto) as_reference(T& t) { return t; }
}
然后编写静态init()函数,返回decltype(auto):
static decltype(auto) init()
{
static bar b;
return notstd::as_reference(b);
}
完整演示:
namespace notstd
{
template<class T>
decltype(auto) as_reference(T& t) { return t; }
}
struct bar {
bar() = default;
bar(bar const&) = delete;
bar& operator=(bar const&) = delete;
};
struct foo
{
//...
static decltype(auto) init()
{
static bar b;
return notstd::as_reference(b);
}
};
int main()
{
auto& b = foo::init();
// won't compile == safe
// auto b2 = foo::init();
}
Skypjack正确地指出init()
可以在没有notstd::as_reference()
的情况下正确编写:
static decltype(auto) init()
{
static bar b;
return (b);
}
返回(b)
周围的括号会强制编译器返回引用。
我对这种方法的问题在于,c ++开发人员经常会对这一点感到惊讶,因此很容易被经验不足的代码维护者忽略。
我的感觉是return notstd::as_reference(b);
明确表达了对代码维护者的意图,就像std::move()
那样。
答案 2 :(得分:1)
如果您想使用单身,请正确使用
class Singleton
{
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator =(const Singleton&) = delete;
private:
Singleton() { /*acquire*/ }
~Singleton() { /*Release*/ }
};
所以你不能创建副本
auto instance = Singleton::getInstance(); // Fail
你可以使用实例。
auto& instance = Singleton::getInstance(); // ok.
但是如果你想要作用范围的RAII而不是单身,你可以做
struct Foo {
Foo() { std::cout << "acquire\n"; }
~Foo(){ std::cout << "release\n"; }
Foo(const Foo&) = delete;
Foo& operator =(const Foo&) = delete;
static Foo init() { return {}; }
};
使用
auto&& foo = Foo::init(); // ok.
仍然禁止复制
auto foo = Foo::init(); // Fail.
答案 3 :(得分:1)
最好的,最惯用的,可读的,不足为奇的事情是=delete
复制构造函数和复制赋值运算符,只需像其他人一样返回引用。
但是,看到你提出了智能指针......
它可能也可以返回
shared_ptr
,但这将涉及不必要的动态内存分配,更糟糕的是,它将是错误的&#34;在我看来。您不分享所有权,您只能被允许查看其他人拥有的内容。原始指针?正确的语义,但......呃。
原始指针在这里是完全可以接受的。如果你不喜欢这样,你可以在&#34;指针&#34;之后有很多选项。思路。
您可以使用没有动态内存的shared_ptr
和自定义删除器:
struct foo {
static shared_ptr<bar> init() { static bar b; return { &b, []()noexcept{} }; }
}
虽然来电者没有&#34;分享&#34;所有权,不清楚当删除者是无操作时,所有权甚至意味着什么。
您可以使用weak_ptr
持有对shared_ptr
管理的对象的引用:
struct foo {
static weak_ptr<bar> init() { static bar b; return { &b, []()noexcept{} }; }
}
但是考虑到shared_ptr
析构函数是一个无操作符,这与前面的示例实际上并没有什么不同,它只是对用户施加了对.lock()
的不必要的调用。 / p>
您可以使用没有动态内存的unique_ptr
和自定义删除器:
struct noop_deleter { void operator()() const noexcept {} };
template <typename T> using singleton_ptr = std::unique_ptr<T, noop_deleter>;
struct foo {
static singleton_ptr<bar> init() { static bar b; return { &b, {} }; }
}
这样做的好处是不需要管理无意义的引用计数,但语义意义也不是一个完美的契合:调用者不会假设唯一所有权,无论所有权真正意味着什么。
在库基础知识TS v2中,您可以使用observer_ptr
,它只是一个表示非拥有意图的原始指针:
struct foo {
static auto init() { static bar b; return experimental::observer_ptr{&b}; }
}
如果您不喜欢这些选项,您当然可以定义自己的智能指针类型。
在未来的标准中,您可以定义一个&#34;智能参考&#34;通过使用重载reference_wrapper
来使用.get()
而不是operator.
。
答案 4 :(得分:0)
有人会在一天内输错,然后我们可能会或可能不会在如此多的代码中注意到它,尽管我们都知道我们应该使用auto&amp;而不是自动。
最方便但非常危险的解决方案是使用派生类,因为它违反了严格别名规则。
$str = '123ABC5678';
$pattern_src = '@(.{2})(.{3})(.{2})(.{2})(.{1})@';
$pattern_rpl = "$1.$2-$3-$4.$5";
$res = preg_replace($pattern_src, $pattern_rpl, $str);
//$res eq 12.3AB-C5-67.8