(возможно, связано с Как реализовать метод C ++, который создает новый объект и возвращает ссылку на него, который о чем-то другом, но случайно содержит почти точно такой же код)

Я хотел бы вернуть ссылку на статический локальный объект из статической функции. Я, конечно, могу заставить его работать, но это не так красиво, как хотелось бы.

Можно ли это улучшить?

Фон

У меня есть пара классов, которые ничего не делают, кроме получения или инициализации ресурса четко определенным и надежным образом и выпуска его. Им даже не нужно много знать о самом ресурсе, но пользователь все равно может захотеть каким-то образом запросить некоторую информацию.
Конечно, это делается тривиально:

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 */ });
}

За исключением того, что сейчас делать запросы немного сложнее, и это не так красиво, как мне нравится. Если вы предпочитаете Страуструпа, а не Александреску, вы можете вместо этого включить GSL и использовать некоторую смесь, включающую final_act. Без разницы.

В идеале я хотел бы написать что-то вроде:

int main()
{
    auto blah = foo::init();
}

Где вы возвращаете ссылку на объект, который вы можете запросить, если хотите это сделать. Или игнорируйте это, или что-то еще Я сразу же подумал: Легко, это всего лишь замаскированный синглтон Мейера . Таким образом:

struct foo
{
//...
    static bar& init() { static bar b; return b; }
};

Это оно! Мертвая простота и совершенство. foo создается, когда вы вызываете init, вы получаете в ответ bar, который вы можете запросить для статистики, и это ссылка, поэтому вы не являетесь владельцем, а {{X3} } автоматически очищается в конце.

Кроме...

Проблема

Конечно, это не может быть так просто, и любой, кто когда-либо использовал диапазон для с auto, знает, что вам нужно написать auto&, если вы не хотите копии-сюрпризы. Но, увы, один только auto выглядел так совершенно невинно, что я об этом не подумал. Кроме того, я явно возвращаю ссылку, так что что может auto захватывать, кроме ссылки!

Результат: сделана копия (из какой? Предположительно из возвращенной ссылки?), Которая, конечно, имеет ограниченный срок жизни. Вызывается конструктор копирования по умолчанию (безвредный, ничего не делает), в конечном итоге копия выходит за пределы области видимости, и контексты освобождаются в середине операции, все перестает работать. В конце программы деструктор вызывается снова. Kaboooom. Ха, как это случилось.

Очевидное (ну, не такое очевидное в первую секунду!) Решение - написать:

auto& blah = foo::init();

Это работает, и работает нормально. Проблема решена, за исключением того, что это некрасиво, и люди могут случайно сделать это неправильно, как я. Разве мы не можем обойтись без дополнительного амперсанда?

Вероятно, также можно было бы вернуть shared_ptr, но это потребовало бы ненужного распределения динамической памяти и, что еще хуже, было бы "неправильно" в моем восприятии. Вы не разделяете собственность, вам просто разрешено просматривать то, что принадлежит кому-то другому. Необработанный указатель? Правильно по семантике, но ... тьфу.

Удалив конструктор копирования, я могу предотвратить попадание невинных пользователей в ловушку "забытый &" (это приведет к ошибке компилятора).

Однако это все еще менее красиво, чем мне хотелось бы. Должен быть способ передать «Это возвращаемое значение должно использоваться как ссылка» компилятору? Что-то вроде return std::as_reference(b);?

Я подумал о каком-то трюке, включающем «перемещение» объекта без его реального перемещения, но не только компилятор почти наверняка не позволит вам перемещать статический локальный объект вообще, но если вам удастся это сделать, вы либо смените владельца, либо или с "поддельным ходом" конструктор перемещения снова вызовите деструктор дважды. Так что это не решение.

Есть ли способ получше и красивее, или мне просто нужно жить с написанием auto&?

8
Damon 6 Сен 2016 в 16:24

5 ответов

Лучший ответ

Что-то вроде return 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.

5
Nicol Bolas 6 Сен 2016 в 14:08

Принуждение пользователя к захвату по ссылке - это трехэтапный процесс.

Во-первых, сделайте возвращаемый объект недоступным для копирования:

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();
}

Скайпджек правильно заметил, что init() можно так же правильно написать без notstd::as_reference():

static decltype(auto) init() 
{ 
    static bar b; 
    return (b); 
}

Скобки вокруг возврата (b) заставляют компилятор возвращать ссылку.

Моя проблема с этим подходом заключается в том, что разработчики C ++ часто удивляются, узнав об этом, поэтому менее опытный сопровождающий код может легко пропустить это.

Мне кажется, что return notstd::as_reference(b); явно выражает намерение разработчикам кода, как и std::move().

5
Richard Hodges 6 Сен 2016 в 14:47

Лучшее, наиболее идиоматичное, читаемое и неудивительное, что можно сделать, - это =delete конструктор копирования и оператор присваивания копии и просто вернуть ссылку, как и все остальные.

Но, увидев, как вы подняли умные указатели ...

Вероятно, также можно было бы вернуть shared_ptr, но это потребовало бы ненужного распределения динамической памяти и, что еще хуже, было бы "неправильно" в моем восприятии. Вы не разделяете собственность, вам просто разрешено просматривать то, что принадлежит кому-то другому. Необработанный указатель? Правильно по семантике, но ... тьфу.

Необработанный указатель здесь был бы вполне приемлемым. Если вам это не нравится, у вас есть несколько вариантов, следующих за ходом мыслей о «указателях».

Вы можете использовать shared_ptr без динамической памяти, с настраиваемым средством удаления:

struct foo {
    static shared_ptr<bar> init() { static bar b; return { &b, []()noexcept{} }; }
}

Хотя вызывающий объект не "разделяет" владение, неясно, что означает владение даже в том случае, если удалитель не работает.

Вы можете использовать weak_ptr, содержащий ссылку на объект, управляемый shared_ptr:

struct foo {
    static weak_ptr<bar> init() { static bar b; return { &b, []()noexcept{} }; }
}

Но, учитывая, что деструктор shared_ptr не работает, он ничем не отличается от предыдущего примера, а просто накладывает на пользователя ненужный вызов .lock().

Вы можете использовать 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}; }
}

Если вам не нравится ни один из этих вариантов, вы, конечно, можете определить свой собственный тип интеллектуального указателя.

В будущем стандарте вы сможете определить «умную ссылку», которая будет работать как reference_wrapper без .get(), используя перегруженный operator..

1
Oktalist 6 Сен 2016 в 20:13

Если вы хотите использовать синглтон, используйте его правильно

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

Где вы можете использовать instance.

auto& instance = Singleton::getInstance(); // ok.

Но если вам нужен RAII с ограниченным доступом вместо singleton, вы можете сделать

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.
1
Jarod42 7 Сен 2016 в 07:38

Кто-то сделает опечатку за один день, и тогда мы можем или не можем заметить это во многих кодах, хотя все мы знаем, что нам следует использовать auto & вместо auto.

Наиболее удобное, но очень опасное решение - использование производного класса, так как он нарушает правило строгого алиасинга.

struct foo
{
private:
    //Make these ref wrapper private so that user can't use it directly.
    template<class T>
    class Pref : public T {
    private:
        //Make ctor private so that we can issue a compiler error when 
        //someone typo an auto. 
        Pref(const Pref&) = default;
        Pref(Pref&&) = default;
        Pref& operator=(const Pref&) = default;
        Pref& operator=(Pref&&) = default;
    };
public:
    static Pref<bar>& init() { 
        static bar b; 
        return static_cast<Pref<bar>&>(b); 
    }
    ///....use Pref<bar>& as well as bar&.
};
0
Landersing 17 Ноя 2016 в 13:35