Указатели на размещенный в стеке объект и перемещение-конструирование

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

Моя проблема довольно общая, но, кажется, ее легче объяснить на конкретном простом примере. Итак, представьте, что я хочу смоделировать потребление электроэнергии в офисе на протяжении всего времени. Предположим, что есть только свет и отопление.

class Simulation {
    public:
        Simulation(Time const& t, double lightMaxPower, double heatingMaxPower)
            : time(t)
            , light(&time,lightMaxPower) 
            , heating(&time,heatingMaxPower) {}

    private:
        Time time; // Note : stack-allocated
        Light light;
        Heating heating;
};

class Light {
    public:
        Light(Time const* time, double lightMaxPower)
            : timePtr(time)
            , lightMaxPower(lightMaxPower) {}

        bool isOn() const {
            if (timePtr->isNight()) {
                return true;
            } else {
                return false;
            }
        }
        double power() const {
            if (isOn()) {
                return lightMaxPower;
            } else {
                return 0.;
            }
    private:
        Time const* timePtr; // Note : non-owning pointer
        double lightMaxPower;
};

// Same kind of stuff for Heating

Важными моментами являются:

1.Time не может быть перемещен, чтобы быть членом данных Light или же Heating так как его изменение не происходит ни от одного из этих классов.

2.Time не нужно явно передавать в качестве параметра Light, Действительно, там может быть ссылка на Light в любой части программы, которая не хочет предоставлять Time в качестве параметра.

class SimulationBuilder {
    public:
        Simulation build() {
            Time time("2015/01/01-12:34:56");
            double lightMaxPower = 42.;
            double heatingMaxPower = 43.;
            return Simulation(time,lightMaxPower,heatingMaxPower);
        }
};

int main() {
    SimulationBuilder builder;
    auto simulation = builder.build();

    WeaklyRelatedPartOfTheProgram lightConsumptionReport;

    lightConsumptionReport.editReport((simulation.getLight())); // No need to supply Time information 

    return 0;
}

Сейчас, Simulation идеально найти до тех пор, пока он не создан для копирования / перемещения. Потому что, если это так, Light также будет создан экземпляр copy / move, и по умолчанию указатель на Time будет указывать на Time в старом Simulation экземпляр, который копируется / перемещается из. Тем не мение, Simulation на самом деле копирование / перемещение строится между оператором возврата в SimulationBuilder::build()и создание объекта вmain()

В настоящее время существует несколько способов решения проблемы:

1: Положитесь на копию elision. В этом случае (и в моем реальном коде)разрешение на копирование, по-видимому, разрешено стандартом. Но это не обязательно, и, по сути, он не исключен лягушкой -O3. Чтобы быть более точным, лягушатники Simulation копировать, но вызывает перемещение ctor для Light, Также обратите внимание, что время, зависящее от реализации, не является надежным.

2: Определить ход-ctor в Simulation:

Simulation::Simulation(Simulation&& old) 
    : time(old.time)
    , light(old.light)
    , heating(old.heating)
{
    light.resetTimePtr(&time);
    heating.resetTimePtr(&time);
}

Light::resetTimePtr(Time const* t) {
    timePtr = t;
}

Это работает, но большая проблема здесь в том, что это ослабляет инкапсуляцию: теперь Simulation должен знать, что Light нужно больше информации во время переезда. В этом упрощенном примере это не так уж плохо, но представьте timePtr не прямо в Light но в одном из его суб-суб-суб-член. Тогда я должен был бы написать

Simulation::Simulation(Simulation&& old) 
    : time(old.time)
    , subStruct(old.subStruct)
{
    subStruct.getSubMember().getSubMember().getSubMember().resetTimePtr(&time);
}

который полностью нарушает инкапсуляцию и закон Деметры. Даже при делегировании функций я нахожу это ужасным.

3: использовать какой-то шаблон наблюдателя, где Time наблюдается Light и отправляет сообщение, когда оно построено так, чтобы Light изменить его указатель при получении сообщения. Я должен признаться, что мне лень писать полный пример этого, но я думаю, что это будет настолько тяжело, что я не уверен, что дополнительная сложность того стоит.

4: Использовать указатель Simulation:

class Simulation {
    private:
        std::unique_ptr<Time> const time; // Note : heap-allocated
};

Теперь когда Simulation перемещен, Time памяти нет, поэтому указатель в Light не признан недействительным На самом деле это то, что делает почти любой другой объектно-ориентированный язык, так как все объекты создаются в куче. Сейчас я предпочитаю это решение, но все же думаю, что оно не идеально: распределение кучи может быть медленным, но, что более важно, оно просто не кажется идиоматичным. Я слышал, как Б. Страуструп сказал, что вы не должны использовать указатель, когда он не нужен и нужен, значит более или менее полиморфный.

5: Построить Simulation на месте, без возврата SimulationBuilder (Затем скопируйте / переместите центр / назначение в Simulation тогда все можно удалить). Например

class Simulation {
    public:
        Simulation(SimulationBuilder const& builder) {
            builder.build(*this);
        }

    private:
        Time time; // Note : stack-allocated
        Light light;
        Heating heating;
        ...
};



class SimulationBuilder {
    public:
        void build(Simulation& simulation) {

            simulation.time("2015/01/01-12:34:56");
            simulation.lightMaxPower = 42.;
            simulation.heatingMaxPower = 43.;
    }
};

Теперь мои вопросы следующие:

1: Какое решение вы бы использовали? Вы думаете о другом?

2: Как вы думаете, что-то не так в оригинальном дизайне? Что бы вы сделали, чтобы это исправить?

3: Вы когда-нибудь сталкивались с такой моделью? Я нахожу это довольно распространенным в моем коде. Как правило, это не проблема, так как Time действительно полиморфный и, следовательно, выделяется куча.

4. Возвращаясь к корню проблемы: "Нет необходимости перемещать, я только хочу, чтобы на месте был создан неподвижный объект, но компилятор не позволит мне сделать это", почему нет простое решение на C++ и есть ли решение на другом языке?

4 ответа

Если все классы нуждаются в доступе к одной и той же const (и, следовательно, неизменяемой) функции, у вас есть (как минимум) 2 варианта, чтобы сделать код чистым и поддерживаемым:

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

  2. хранить std::shared_ptr<const SharedFeature> вместо ссылки на const - это работает во всех случаях, почти без дополнительных затрат. std::shared_ptr конечно, полностью осознает движение.

В комментарии к вашему второму решению вы говорите, что оно ослабляет инкапсуляцию, потому что Simulation должен знать, что Light нужно больше информации во время переезда. Я думаю, что это наоборот. Light необходимо знать, что используется в контексте, где предоставленная ссылка на Time объект может стать недействительным во время LightПожизненная Что не хорошо, потому что это заставляет дизайн Light основанный на том, как это используется, не основанный на том, что это должно сделать.

Передача ссылки между двумя объектами создает (или должен создавать) договор между ними. При передаче ссылки на функцию эта ссылка должна быть действительной до тех пор, пока вызываемая функция не вернется. При передаче ссылки на конструктор объекта эта ссылка должна быть действительной в течение всего времени существования построенного объекта. Объект, передающий ссылку, отвечает за ее достоверность. Если вы не будете следовать этому, вы можете создать очень трудно прослеживаемые отношения между пользователем ссылки и сущностью, поддерживающей время жизни ссылочного объекта. В вашем примере Simulation не может соблюдать договор между ним и Light объект, который он создает при перемещении. Со времени существования Light Объект тесно связан с временем жизни Simulation У объекта есть 3 способа решения этой проблемы:

1) ваше решение № 2)

2) передать ссылку на Time объект для конструктора Simulation, Если вы принимаете договор между Simulation и внешняя сущность, передающая ссылку, является надежной, поэтому будет заключен договор между Simulation а также Light, Однако вы можете рассматривать объект Time как внутреннюю деталь Simulation объект и таким образом вы нарушите инкапсуляцию.

3) сделать Simulation непоколебимы. Поскольку C++(11/14) не имеет никаких "методов конструктора на месте" (не знаю, насколько хорош этот термин), вы не можете создать объект на месте, возвращая его из какой-то функции. Copy/Move-elision в настоящее время является оптимизацией, а не функцией. Для этого вы можете использовать свое решение 5) или использовать лямбды, например:

class SimulationBuilder {
    public:
        template< typename SimOp >
        void withNewSimulation(const SimOp& simOp) {
            Time time("2015/01/01-12:34:56");
            double lightMaxPower = 42.;
            double heatingMaxPower = 43.;
            Simulation simulation(time,lightMaxPower,heatingMaxPower);
            simOp( simulation );
        }
};

int main() {
    SimulationBuilder builder;

    builder.withNewSimulation([] (Simulation& simulation) {

        WeaklyRelatedPartOfTheProgram lightConsumptionReport;

        lightConsumptionReport.editReport((simulation.getLight())); // No need to supply Time information
    } 

    return 0;
}

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

1: Какое решение вы бы использовали? Вы думаете о другом?

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

  • Симуляция превращается в синглтон.

build() функция в SimulationBuilder перемещается в Simulation, Конструктор для Simulation приватизируется, и ваш главный вызов становится Simulation * builder = Simulation::build();, Симуляция также получает новую переменную static Simulation * _Instance;и мы вносим несколько изменений в Simulation::build()

class Simulation
{
public:
static Simulation * build()
{
    // Note: If you don't need a singleton, just create and return a pointer here.
    if(_Instance == nullptr)
    {
        Time time("2015/01/01-12:34:56");
        double lightMaxPower = 42.;
        double heatingMaxPower = 43.;
        _Instance = new Simulation(time, lightMaxPower, heatingMaxPower);
    }

    return _Instance;
}
private:
    static Simulation * _Instance;
}

Simulation * Simulation::_Instance = nullptr;
  • Освещение и обогрев объектов предоставляются как фабрика.

Эта мысль бесполезна, если в симуляции будет только 2 объекта. Но если вы собираетесь управлять 1...N объектами и несколькими типами, тогда я настоятельно рекомендую вам использовать фабрику и динамический список (vector, deque и т. Д.). Вам нужно было бы сделать так, чтобы Light и Heating наследовали от общего шаблона, настроить для регистрации этих классов с фабрикой, настроить фабрику так, чтобы она была шаблонной, а экземпляр фабрики мог создавать только объекты определенного шаблона и инициализировать фабрика для объекта моделирования. В основном завод будет выглядеть примерно так

template<class T>
class Factory
{
    // I made this a singleton so you don't have to worry about 
    // which instance of the factory creates which product.
    static std::shared_ptr<Factory<T>> _Instance;
    // This map just needs a key, and a pointer to a constructor function.
    std::map<std::string, std::function< T * (void)>> m_Objects;

public:
    ~Factory() {}

    static std::shared_ptr<Factory<T>> CreateFactory()
    {
        // Hey create a singleton factory here. Good Luck.
        return _Instance;
    }

    // This will register a predefined function definition.
    template<typename P>
    void Register(std::string name)
    {
        m_Objects[name] = [](void) -> P * return new P(); };
    }

    // This could be tweaked to register an unknown function definition,
    void Register(std::string name, std::function<T * (void)> constructor)
    {
        m_Objects[name] = constructor;
    }

    std::shared_ptr<T> GetProduct(std::string name)
    {            
        auto it = m_Objects.find(name);
        if(it != m_Objects.end())
        {
            return std::shared_ptr<T>(it->second());
        }

        return nullptr;
    }
}

// We need to instantiate the singleton instance for this type.
template<class T>
std::shared_ptr<Factory<T>> Factory<T>::_Instance = nullptr;

Это может показаться немного странным, но это действительно делает создание шаблонных объектов увлекательным. Вы можете зарегистрировать их, сделав это:

// To load a product we would call it like this:
pFactory.get()->Register<Light>("Light");
pFactory.get()->Register<Heating>("Heating");

И затем, когда вам нужно получить объект, все, что вам нужно, это:

std::shared_ptr<Light> light = pFactory.get()->GetProduct("Light");

2: Как вы думаете, что-то не так в оригинальном дизайне? Что бы вы сделали, чтобы это исправить?

Да, конечно, но, к сожалению, у меня не так много подробностей из моего ответа на пункт 1.

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

Кроме того, если бы я по-настоящему беспокоился о скорости таких вещей, как распределение памяти, то я бы принял во внимание некоторые вещи из моего прогона профилирования, например, сколько раз объект создавался по сравнению с временем жизни объектов, надеюсь, мой сеанс профилирования сказал мне этот. Объект как твой Simulation класс должен быть создан не более 1 раза для данного прогона симуляции, в то время как объект типа Light может быть создан 0..N раз во время пробега. Итак, я бы сосредоточился на том, как создание Light объекты повлияли на мою производительность.


3: Вы когда-нибудь сталкивались с такой моделью? Я нахожу это довольно распространенным в моем коде. Однако, в общем, это не проблема, так как Time действительно полиморфен и, следовательно, выделяется кучей.

Я обычно не вижу, чтобы объекты симуляции поддерживали способ видеть текущие переменные изменения состояния, такие как Time, Обычно я вижу, что объект поддерживает свое состояние и обновляется только тогда, когда изменение времени происходит с помощью такой функции, как SetState(Time & t){...}, Если вы думаете об этом, это имеет смысл. Симуляция - это способ увидеть изменение объектов, заданных конкретным параметром (ами), и этот параметр не требуется для того, чтобы объект сообщал о своем состоянии. Таким образом, объект должен обновляться только одной функцией и поддерживать свое состояние между вызовами функций.

// This little snippet is to give you an example of how update the state.
// I guess you could also do a publish subscribe for the SetState function.
class Light
{
public:
    Light(double maxPower) 
        : currPower(0.0)
        , maxPower(maxPower)
    {}

    void SetState(const Time & t)
    {
        currentPower = t.isNight() ? maxPower : 0.0;
    }

    double GetCurrentPower() const
    {
        return currentPower;
    }
private:
    double currentPower;
    double maxPower;
}

Удержание объекта от выполнения его собственной проверки на Времени помогает уменьшить многопоточные напряжения, такие как "Как я могу обработать случай, когда время изменяется, и делает недействительным мое состояние включения / выключения после того, как я прочитал время, но до того, как я возвратил свое состояние?"


4. Возвращаясь к корню проблемы: "Нет необходимости перемещать, я только хочу, чтобы на месте был создан неподвижный объект, но компилятор не позволит мне сделать это", почему нет простое решение на C++ и есть ли решение на другом языке?

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

РЕДАКТИРОВАТЬ: Из-за именования классов и порядка я полностью упустил тот факт, что ваши два класса не связаны между собой.

Очень трудно помочь вам с таким абстрактным понятием, как "особенность", но я собираюсь полностью изменить свою мысль здесь. Я бы предложил перенести владение функцией в MySubStruct, Теперь копирование и перемещение будут работать нормально, потому что только MySubStruct знает об этом и умеет делать правильные копии. Сейчас MyClass должен иметь возможность работать с функцией. Итак, где необходимо просто добавить делегирование MySubStruct: subStruct.do_something_with_feature(params);,

Если ваша функция нуждается в элементах данных из обеих подструктур И MyClass тогда я думаю, что вы неправильно распределили обязанности и вам нужно пересмотреть весь путь до разделения MyClass а также MySubStruct,

Оригинальный ответ, основанный на предположении, что MySubStruct был ребенком MyClass:

Я считаю, что правильный ответ, чтобы удалить featurePtr от ребенка и обеспечить надлежащий защищенный интерфейс для функции в родительском (примечание: я действительно имею в виду абстрактный интерфейс здесь, а не просто get_feature() функция). Тогда родитель не должен знать о детях, и ребенок может использовать эту функцию по мере необходимости.

Чтобы быть полностью ясным: MySubStruct не будет знать, что родительский класс даже имеет член с именем feature, Например, возможно что-то вроде этого:

Другие вопросы по тегам