提示编译器返回一个引用“' auto'表现

时间:2016-09-06 13:24:08

标签: c++ c++14

(可能与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&

5 个答案:

答案 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()

然而,无论如何,我相信你的想法是错误的。原因是autoauto&每个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