Получение unique_ptr для класса, который наследует enable_shared_from_this

Обычно я предпочитаю возвращатьсяunique_ptr с заводов. Недавно я пришел к проблеме возвращенияunique_ptr для класса, который наследуетenable_shared_from_this, Пользователи этого класса могут случайно вызвать звонокshared_from_this(), хотя он не принадлежит никомуshared_ptr, что приводит кstd::bad_weak_ptr исключение (или неопределенное поведение до C ++ 17, которое обычно реализуется как исключение).

Простая версия кода:

class Foo: public enable_shared_from_this<Foo> {
    string name;
    Foo(const string& _name) : name(_name) {}
public:
    static unique_ptr<Foo> create(const string& name) {
        return std::unique_ptr<Foo>(new Foo(name));
    }
    shared_ptr<Foo> get_shared() {return shared_from_this();}
    void doIt()const {cout << "Foo::doIt() <" << name << '>' << endl;}
    virtual ~Foo() {cout << "~Foo() <" << name << '>' << endl;}
};

int main() {
    // ok behavior
    auto pb1 = Foo::create("pb1");
    pb1->doIt();
    shared_ptr<Foo> pb2 = shared_ptr<Foo>(std::move(pb1));
    shared_ptr<Foo> pb3 = pb2->get_shared();
    pb3->doIt();

    // bad behavior
    auto pb4 = Foo::create("pb4");
    pb4->doIt();
    shared_ptr<Foo> pb5 = pb4->get_shared(); // exception
    pb5->doIt();    
}

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

Вопрос в том, как добиться всего следующего:

разрешить фабрике вернутьсяunique_ptrразрешатьunique_ptr этого класса, чтобы стать общимразрешатьshared_ptr этого класса, чтобы получить общие копии (черезshared_from_this())избежать неудачи, когдаunique_ptr этого класса пытается получить общий доступ от этого (вызовget_shared в приведенном выше примере)

Пункты с 1 по 3 выполняются с кодом выше,проблема с пунктом 4.

 Steve Lorimer27 июн. 2016 г., 20:50
@AmirKirsh, не зная вашего конкретного дизайна, сложно сказать. Что я могу сказать, так это то, что из-за вашего дизайна сложно сделать правильный выбор - клиенты могут позвонитьshared_from_this когда они вunique_ptr установка. Скотт МейерВажнейшее руководство по дизайну являетсяСделать интерфейсы простыми в использовании, правильными и трудными в использовании, Таким образом, отделение однопоточной / многопоточной части системы отPet/Cat/Salamandra часть, вероятно, будет хорошей идеей - разделение интересов
 Amir Kirsh27 июн. 2016 г., 21:03
@SteveLorimer Это именно то, что я искал, чтобы убедиться, что клиенты могут достичь точки, когда требуется совместное использование, и это будет работать прозрачно, без особой заботы, если они с общим питомцем или с уникальным питомцем (т.е.делая интерфейс простым в использовании так далее.). Разделение однопоточных / многопоточных частей системы не означает, что домашние животные не склонны посещать обе части, а в некоторых случаях начинаются с одной и заканчиваются с другой.
 kfsone27 июн. 2016 г., 19:20
Вашcreate может быть лучшеreturn std::make_unique<Foo>(name);нет?
 Steve Lorimer27 июн. 2016 г., 20:31
@AmirKirsh, если у вас естьunique_ptr из которого вам нужно создатьshared_ptrтогдаunique_ptr не уникален, и поэтому вы использовали не тот инструмент для работы.
 utnapistim27 июн. 2016 г., 17:43
Можете ли вы добавить редактирование, чтобы объяснить ваш вариант использования? Я пытаюсь представить, что все, что я могу придумать, говорит мне, что у вас есть проблема с дизайном (или проблема с x-y).
 Amir Kirsh27 июн. 2016 г., 20:26
@SteveLorimer сценарий использования прост: есть структура данных (предположим: Cat), которая в некоторых случаях отправляется в цепочку обработки, которая допускает уникальные указатели (однопоточная обработка), где существует другая цепочка обработки, в которой другой Cat ( Тем не менее, Cat) должен быть отправлен без четкого владения (в данном случае это многопоточная обработка).
 Amir Kirsh27 июн. 2016 г., 20:18
@kfsone закрытый конструктор предотвращает использованиеstd::make_unique<Foo>(name), Есть несколько вариантов редизайна, чтобы разрешить это, но это не было главной проблемой здесь.
 Steve Lorimer27 июн. 2016 г., 17:43
Я бы с подозрением относился к дизайну, в котором общие и уникальные указатели смешаны в одной структуре данных.
 Amir Kirsh27 июн. 2016 г., 20:44
@SteveLorimer хорошо, когда родился Кот, он не знает, нужно ли ему делиться или нет. Процесс, через который проходит Кошка, может в определенный момент решить, что есть необходимость поделиться. Очевидно, это правильная мера для фабрики всегда возвращатьunique_ptr (либоunique_ptr<Cat> илиunique_ptr<Pet> который на самом деле может бытьunique_ptr<Salamandra>). Требование о том, чтобы Cat или фабрика питомцев знали заранее, будет ли необходимость делиться или нет, не является общим правилом проектирования для всех случаев, я могу представить себе случаи, которые подходят, и случаи, которых нет.

Ответы на вопрос(6)

Решение Вопроса

Используйте несколько статических заводских функций и функций преобразования. Чтобы ответить на ваши комментарии, я добавилget_shared поддерживать копирование общего указателя. Это компилируется и доступно здесь:http://ideone.com/UqIi3k

#include <iostream>
#include <memory>

class Foo
{
    std::string name;
    Foo(const std::string& _name) : name(_name) {}
public:
    void doIt() const { std::cout << "Foo::doIt() <" << name << '>' << std::endl;}
    virtual ~Foo() { std::cout << "~Foo() <" << name << '>' << std::endl;}

    static std::unique_ptr<Foo> create_unique(const std::string & _name) {
        return std::unique_ptr<Foo>(new Foo(_name));
    }
    static std::shared_ptr<Foo> create_shared(const std::string & _name) { 
        return std::shared_ptr<Foo>(new Foo(_name));
    }

    static std::shared_ptr<Foo> to_shared(std::unique_ptr<Foo> &&ptr ) { 
         return std::shared_ptr<Foo>(std::move(ptr)); 
    }
    static std::shared_ptr<Foo> get_shared(const std::shared_ptr<Foo> &ptr) {
         return std::shared_ptr<Foo>(std::move(ptr));
    }
};

int main() {
    // ok behavior
    auto pb1 = Foo::create_unique("pb1");
    pb1->doIt();
    std::shared_ptr<Foo> pb2 = Foo::get_shared(std::move(pb1));
    //note the change below
    std::shared_ptr<Foo> pb3 = Foo::get_shared(pb2);
    pb3->doIt();

    // also OK behavior
    auto pb4 = Foo::create_unique("pb4");
    pb4->doIt();
    std::shared_ptr<Foo> pb5 = Foo::to_shared(std::move(pb4)); // no exception now
    pb5->doIt();

    std::shared_ptr<Foo> pb6 = Foo::create_shared("pb6");
    pb6->doIt();
    std::shared_ptr<Foo> pb7 = std::shared_ptr<Foo>(pb5);
    pb7->doIt();
    return 0;
}
 Amir Kirsh28 июн. 2016 г., 17:39
Мне нравится это решение. Это лучше, чем у меня, так как не требует наследования отenable_shared_from_this и это просто.
 Jon Harper28 июн. 2016 г., 13:42
@ Амир Я рассмотрел вашу проблему и предоставил рабочий пример моего решения.

Это именно то, что я искал, чтобы убедиться, что клиенты могут достичь точки, когда требуется совместное использование, и это будет работать прозрачно, без особой заботы, если они с общим питомцем или с уникальным питомцем (т. Е. Упрощают правильное использование интерфейса и т. Д.). .).

Это звучит как проблема X-Y для меня.

Чтобы «убедиться, что клиенты могут делиться при необходимости», включите его в отдельный инструмент и поместите в свой набор инструментов (редактировать: но все равно кажется, что у тебя проблема с х-у)

namespace tools
{

    /// @brief hides the details of sharing a unique pointer
    ///        behind a controlled point of access
    ///
    /// to make sure that clients can share if required, use this as a
    /// return type
    template<typename T>
    class pointer final
    {
    public:

        // @note: implicit on purpose (to enable construction on return,
        //        in the client code below)
        pointer(std::unique_ptr<T> value);

        // @note: implicit on purpose (to enable construction on return,
        //        in the client code below)
        pointer(std::shared_ptr<T> value);

        T* const operator->() const { return get(); }

        /// @note copy&swap
        pointer& operator=(pointer p)
        {
            using std::swap;
            swap(value1, p.value1);
            swap(value2, p.value2);
            return *this;
        }

        // example copy
        pointer(const pointer<T>& value)
        : value1{}, value2{ value.share() }
        {
        }

        // example move
        pointer(pointer<T>&& tmp)
        : value1{ std::move(tmp.value1) }, value2{ std::move(tmp.value2) }
        {
        }

        /// @note conceptually const, because it doesn't change the address
        ///       it points to
        ///
        /// @post internal implementation is shared
        std::shared_ptr<T> share() const
        {
            if(value2.get())
                return value2;

            value2.reset(value1.release());
                return value2;
        }

        T* const get() const
        {
             if(auto p = value1.get())
                 return p;
             return value2;
        }

    private:
        mutable std::unique_ptr<T> value1;
        mutable std::shared_ptr<T> value2;
    };
}

Ваш клиентский код становится:

class Foo
{
    string name;
    Foo(const string& _name) : name(_name) {}
public:

    using pointer = tools::pointer<Foo>;

    static pointer make_unique(const string& name)
    {
        return std::make_unique<Foo>(name);
    }

    void doIt()const {cout << "Foo::doIt() <" << name << '>' << endl;}

    virtual ~Foo() {cout << "~Foo() <" << name << '>' << endl;}
};

int main() {
    // ok behavior
    auto pb1 = Foo::make_unique("pb1");
    pb1->doIt(); // call through unique pointer
    auto pb2 = pb1.share(); // get copy of shared pointer
    auto pb3 = pb1; // get copy of shared pointer

    auto pb4 = std::move(pb1); // move shared pointer
}
 Amir Kirsh28 июн. 2016 г., 10:57
Вы представляете очень аккуратный подход! Интересно увидеть решение, не используяenable_shared_from_this (или аналогичная механика). Но я все еще скучаю по вещам в вашем решении, например: оператор -> для указателя класса. Можете ли вы добавить ссылку на код? Благодарю.
 Amir Kirsh28 июн. 2016 г., 10:09
Выглядит интересно. Но ты уверен?shared_from_this можно вызвать из статического метода (static pointer make_shared())? Вы можете разместить ссылку на код?
 Jon Harper28 июн. 2016 г., 11:24
Кажется, что слишком барочное решение иметь псевдо-умный класс указателей для управления умными указателями. Это интересно, но добавляет дополнительный уровень абстракции, когда вместо оригинального дизайна могут возникнуть проблемы.
 utnapistim28 июн. 2016 г., 12:00
@JonHarper, я знаю :(. Он отвечает на вопрос, но не на проблему x-y. На практике я, вероятно, просто использовал бы shared_ptr, но я не могу говорить о конкретной проблеме ОП.
 utnapistim28 июн. 2016 г., 11:12
Я добавил семантику копирования и доступ к указателю. Я не могу превратить это в полное решение (я нахожусь в офисе и у меня нет много времени).
 utnapistim28 июн. 2016 г., 10:13
Это не может; Я изменил код, чтобы избежать shared_from_this в целом. Теперь все совместное использование выполняется на уровне указателя. Вы можете добавитьenum class pointer_type { shared, unique } и передать его в качестве аргументаFoo::make_unique (но тогда вам придется переименовать его вmake_pointer или похожие); Затем, основываясь на этом аргументе, вы можете сделатьreturn shared_from_this(name); и вы все равно наследуете от enable_shared_from_this. Хотя лучше сделать это наследство частным.

Проблема с функцией-членомget_shared вопрос в том, что он позволяет звонки какunique_ptr а такжеshared_ptr с трудом различить два, таким образом,unique_ptr разрешено вызывать этот метод и терпит неудачу.

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

class Foo: public enable_shared_from_this<Foo> {
    string name;
    Foo(const string& _name) : name(_name) {}
public:
    static unique_ptr<Foo> create(const string& name) {
        return std::unique_ptr<Foo>(new Foo(name));
    }
    static shared_ptr<Foo> get_shared(unique_ptr<Foo>&& unique_p) {
        return shared_ptr<Foo>(std::move(unique_p));
    }
    static shared_ptr<Foo> get_shared(const shared_ptr<Foo>& shared_p) {
        return shared_p->shared_from_this();
    }
    void doIt()const {cout << "Foo::doIt() <" << name << '>' << endl;}
    virtual ~Foo() {cout << "~Foo() <" << name << '>' << endl;}
};

int main() {
    // ok behavior - almost as before
    auto pb1 = Foo::create("pb1");
    pb1->doIt();
    shared_ptr<Foo> pb2 = shared_ptr<Foo>(std::move(pb1));
    shared_ptr<Foo> pb3 = Foo::get_shared(pb2);
    pb3->doIt();

    // ok behavior!
    auto pb4 = Foo::create("pb4");
    pb4->doIt();
    shared_ptr<Foo> pb5 = Foo::get_shared(std::move(pb4));
    pb5->doIt();    
}

Код:http://coliru.stacked-crooked.com/a/7fd0d462ed486c44

 Amir Kirsh27 июн. 2016 г., 20:33
@ Jarod42 - это подводный камень примера минимального кода ... предположим, что метод, который мы хотим вызвать (смоделирован какget_shared в этом примере) необходимо выполнить несколько вещей, в том числе получить новый общий указатель вызывающей стороны (например, чтобы отправить его процессу в другом потоке, который может пережить этот поток). Мы хотим сделать это прозрачным для того, имел ли вызывающий изначально под рукой unique_ptr или shared_ptr.
 Jarod4227 июн. 2016 г., 18:05
get_shared(const shared_ptr<Foo>& shared_p) в основном бесполезен, так как использованиеshared_from_this() это когда у нас просто есть экземпляр (указатель ссылки), но не в пределах егоshared_ptr.

Как вы обнаружите, когда объект управляетсяshared_ptr, нет (безопасного) способа отменить его. Это сделано специально, так как после совместного использования времени жизни объекта, ни один владелец не может гарантировать, что он когда-либо снова станет единственным владельцем (кроме как во время выполнения средства удаления - я оставлю вас на размышление о том, полезно ли это для вы).

Тем не менее, стандартная библиотека позволяет вамспециализироваться любой шаблон класса std, если он специализирован дляпользовательский класс.

Итак, что вы можете сделать, это:

namespace std {
  template<class Deleter>
  struct unique_ptr<Foo, Deleter> {
    static_assert(!std::is_same<Deleter, Deleter>::value, "sorry, not allowed");
    // or
    static_assert(!sizeof(Deleter), "sorry, not allowed");
    // (thanks to Jarod42 && md5i for cleaning up after me)
  };
}

И теперь ни один unique_ptr не может владеть вашим объектом (который явно предназначен для владения только shared_ptr).

Это применяется во время компиляции.

 md5i27 июн. 2016 г., 19:16
В самом деле. Я обычно используюstatic_assert(!sizeof(Deleter), ...);, Важным моментом является то, что он зависит от одного из аргументов шаблона.
 Richard Hodges28 июн. 2016 г., 08:44
@ChrisBeck, если ничего не зависит от их размера или адреса, оптимизатор может удалить их полностью. Нет, отходов нет.
 Amir Kirsh27 июн. 2016 г., 22:21
@RichardHodges Я перефразирую ваше предложение: единственная причина, чтобы когда-либо происходить изenable_shared_from_this это тот самый классможет вызовshared_from_this, Я утверждаю, что вы можете начать сunique_ptr этого класса, в некоторых случаях оставаясь с уникальным до его смерти (unique_ptr прошел через процесс и не должен был делиться), а в некоторых случаях может потребоваться поделиться (например, необходимость отправить его в другой поток). Опять же, я утверждаю, что это не далеко от необходимости повернутьunique_ptr в некоторых случаяхshared_ptr черезshared_ptr(unique_ptr<Y,Deleter>&&) конструктор.
 Chris Beck28 июн. 2016 г., 08:42
Понятно, я этого не осознавал. Итак, когда мы создаем все эти функциональные объекты без членов, технически они имеют размер один? Даже если после того, как оптимизатор сделан, они не существуют?
 Chris Beck28 июн. 2016 г., 08:45
Я вижу, это интересная тонкость. Это имеет смысл, хотя, я полагаю, очень важно иметь возможность создавать массивы и арифметику указателей.
 Richard Hodges27 июн. 2016 г., 22:30
@AmirKirsh описанный вами вариант использования более правильно обрабатывается двумя классами. То, что вы делаете, создает ловушку для пользователя вашего класса, как при его выводе, так и при манипулировании указателем на него. Я решительный. Не существует разумного основания для дизайна, который вы описываете.
 Richard Hodges27 июн. 2016 г., 21:49
@AmirKirsh единственная причина когда-либо происходить отenable_shared_from_this является то, что сам класс будет вызыватьshared_from_this, Если он делает это, пока не контролируетсяshared_ptrэто логическая ошибка. Для такой возможности нет разумных оснований.
 Richard Hodges27 июн. 2016 г., 21:10
@AmirKirsh, поэтому я бы сказал, что предотвращение создания unique_ptr - это путь. Причина в том, что если класс является производным отenable_shared_from_this, это обязательно непригодно. Там нет никаких аргументов для его создания в первую очередь.
 Richard Hodges27 июн. 2016 г., 23:19
@AmirKirsh конечно, твой код принадлежит тебе. Вы можете делать, как хотите. Я надеюсь, что мне никогда не придется поддерживать это. Удачи.
 Richard Hodges27 июн. 2016 г., 19:24
@ Jarod42 ах да, один раз, когда я не проверяю свою работу в Godbolt перед тем, как оторвать рот ... поправлюсь. благодарю вас.
 Amir Kirsh27 июн. 2016 г., 23:09
@RichardHodges мы, вероятно, согласимся не соглашаться ... для меня два класса звучат как два класса, один для обработки указателя на объект, и другой, когда нам нужен агрегатный тип. И тогда возникает также вопрос, почему существуетshared_ptr(unique_ptr<Y,Deleter>&&) конструктор, еслиunique_ptr а такжеshared_ptr не должны быть смешаны ...?
 Richard Hodges28 июн. 2016 г., 00:17
@AmirKirsh это потому, что класс может принадлежать shared_ptr, не являясь производным от enable_shared_from_this. В случае, если назначение является законным.
 Richard Hodges28 июн. 2016 г., 08:39
@ChrisBeck пустая структура имеет размер как минимум 1. Это так, что массив из них помещает руку один по отдельному адресу. Это в стандарте.
 Chris Beck28 июн. 2016 г., 03:13
Почему это работает? Это не возможно, чтоsizeof(Deleter) == 0 если, например, это пустая структура? Или я что то пропустил
 Richard Hodges28 июн. 2016 г., 09:26
@ChrisBeck также следует, что адрес объекта является уникальным идентификатором.
 Amir Kirsh27 июн. 2016 г., 21:34
@RichardHodges Я не согласен. Это обсуждение, которое я веду с @SteveLorimer выше. Иметь возможность делиться не значит бытьвсегда общий. Аргумент, говорящий, что тип может быть уникальным или общим, делает конструкторshared_ptr(unique_ptr<Y,Deleter>&&) излишний.
 Amir Kirsh27 июн. 2016 г., 23:48
@RichardHodges кто знает? это маленький мир :-) Я вижу, вы игнорируете мой вопрос о том, почему есть необходимостьshared_ptr(unique_ptr<Y,Deleter>&&) конструктор, еслиunique_ptr а такжеshared_ptr не должны смешиваться, и тип может быть уникальным или общим во всех случаях использования.
 Amir Kirsh27 июн. 2016 г., 20:54
@RichardHodges в этом примере мы могли бы заблокировать разумное созданиеunique_ptr<Foo> изменяяcreate способ вернутьshared_ptr<Foo> вместоunique_ptr<Foo> (все еще можно делать глупые вещи, какunique_ptr<Foo> pfoo = unique_ptr<Foo>(some_shared_pointer.get()) но я не ищу предотвращения такой глупости, борьба с таким уровнем глупости бесконечна, лучше найдите человека, который делает это, и освободите его), что я искал, чтобы избежать ошибки, которая может произойти, вызывая метод изunique_ptr что в свою очередь пытается позвонитьshared_from_this.

Извлечение изenable_shared_from_this это обещание, что ваши экземпляры принадлежат (группе)shared_ptr. Не ври об этом, только когда-либо создаватьshared_ptr<Foo> а такженикогда раздаточный материалunique_ptr<Foo>.

Это не стоит будущей боли, когда вы должны распутать «безопасное» использованиеunique_ptr<Foo> из случая, когдаFoo & глубоко в некоторой логике, которая хочет позвонитьshared_from_this ноиногда на самом делеunique_ptr<Foo>.

Может быть, мы можем проверить шаблон вызывающей переменной:

class Foo : public enable_shared_from_this<Foo> {
    string name;
    Foo(const string& _name) : name(_name) {}
public:
    static unique_ptr<Foo> create(const string& name) {
        return std::unique_ptr<Foo>(new Foo(name));
    }
    template <typename T>
        shared_ptr<Foo> get_shared() { return shared_ptr<Foo>(); }
    template <>
    shared_ptr<Foo> get_shared<unique_ptr<Foo>>() { return shared_ptr<Foo>(); }

    template <>
    shared_ptr<Foo> get_shared<shared_ptr<Foo>>() { return shared_from_this(); }

    void doIt()const { cout << "Foo::doIt() <" << name << '>' << endl; }
    virtual ~Foo() { cout << "~Foo() <" << name << '>' << endl; }
};

int main()
{
    // ok behavior
    auto pb1 = Foo::create("pb1");
    pb1->doIt();
    shared_ptr<Foo> pb2{ std::move(pb1) };
    shared_ptr<Foo> pb3 = pb2->get_shared<decltype(pb2)>();
    pb3->doIt();

    // bad behavior
    auto pb4 = Foo::create("pb4");
    pb4->doIt();
        shared_ptr<Foo> pb5 = pb4->get_shared<decltype(pb4)>(); // exception
        if (pb5 != nullptr)
            pb5->doIt();

    return 0;
}

Я не уверен, что это именно то, что вы хотите, но может решить пункт 4, который вы упомянули.

 Raghavendar Reddy28 июн. 2016 г., 11:06
В моей IDE все работает отлично! Во всяком случае, это не имеет значения, точка сделана. хорошего дня!
 Amir Kirsh28 июн. 2016 г., 09:43
Это хорошее решение, однако требуется, чтобы вызывающий абонент сделал более громоздкий вызов (сdecltype). Я предпочитаю свой собственный ответ, который достигает того же самого, просто превращая метод в статический метод, который заставляет объект работать в качестве аргумента.
 Amir Kirsh28 июн. 2016 г., 09:56
Ну, есть еще одна проблема с вашим решением:это не работает, Я исправил некоторые ошибки компиляции, чтобы иметь возможность запустить его, пытаясь вызватьpb5->doIt(); падает (возможно, именно поэтому у васif (pb5 != nullptr)). Вот код:coliru.stacked-crooked.com/a/79fe4af7fe5bbe84

Ваш ответ на вопрос