Отложенное обновление с прокси-объектами и «Избегайте неназванных объектов с пользовательским построением и разрушением» - PullRequest
2 голосов
/ 11 апреля 2019

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

Я решил (или «решил»?) Это требование с помощью прокси. Ниже приведен пример минимального рабочего кода:

#include <iostream>

class complicated
{
public:
    class proxy
    {
    public:
        proxy(complicated& tbu) : to_be_updated(&tbu) {
        }

        ~proxy() {
            if (nullptr != to_be_updated) {
                to_be_updated->update_internal_state();
            }
        }

        // If the user uses this operator, disable update-call in the destructor!
        complicated* operator->() {
            auto* ret = to_be_updated; 
            to_be_updated = nullptr;
            return ret;
        }
    private:
        complicated* to_be_updated;
    };

public:
    proxy set_a(int value) {
        std::cout << "set_a" << std::endl;
        a = value;
        return proxy(*this);
    }

    proxy set_b(int value) {
        std::cout << "set_b" << std::endl;
        b = value;
        return proxy(*this);
    }

    proxy set_c(int value) {
        std::cout << "set_c" << std::endl;
        c = value;
        return proxy(*this);
    }

    void update_internal_state() {
        std::cout << "update" << std::endl;
        expensive_to_compute_internal_state = a + b + c;
    }

private:
    int a;
    int b;
    int c;
    int expensive_to_compute_internal_state;
};

int main()
{
    complicated x;
    x.set_a(1);
    std::cout << std::endl;
    x.set_a(1)->set_b(2);
    std::cout << std::endl;
    x.set_a(1)->set_b(2)->set_c(3);
}

Он производит следующий вывод, который выглядит именно так, как я хотел:

set_a
обновление

set_a
set_b
обновление

set_a
set_b
Set_C
обновление

Мои вопросы: Законен ли мой подход / лучшая практика?
Можно ли полагаться на временные объекты (т. Е. proxy возвращаемые объекты), которые будут уничтожены в точке с запятой?

Я спрашиваю, потому что по какой-то причине у меня плохое предчувствие. Может быть, мое плохое предчувствие связано с предупреждением Visual Studio, которое гласит:

Предупреждение C26444 Избегайте неназванных объектов с пользовательской конструкцией и уничтожение (es.84).

Но, может быть / надеюсь, мои плохие чувства неоправданны, и это предупреждение можно просто проигнорировать?

Что беспокоит меня больше всего: Есть ли случай, когда метод update_internal_state НЕ будет вызван (возможно, из-за неправильного использования моего класса или из-за какой-то оптимизации компилятора или чего-то еще)?

Наконец: Есть ли лучший подход для реализации того, что я пытаюсь достичь с помощью современного C ++?

Ответы [ 3 ]

3 голосов
/ 11 апреля 2019

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

x.set_a(1);
x.set_b(2);

чем

x.set_a(1)->set_b(2);

Я бы предложил сделать сеттеры приватными и добавить класс транзакции друга, чтобы модифицируемый объект выглядел так:

complicated x;
{
    transaction t(x);
    t.set_a(1);
    t.set_b(2);
    // implicit commit may be also done in destructor
    t.commit();
}

Если transaction будет единственным способом изменить complicated - пользователи будут чаще вызывать несколько сеттеров за одну транзакцию.

2 голосов
/ 11 апреля 2019

Я вижу здесь опасность, если у вашего класса есть какие-либо методы, которые не возвращают прокси (или каких-либо открытых членов). Вы отключаете вызов обновления, если используется operator-> прокси (что дает complicated), но это безопасно только в том случае, если при использовании operator-> всегда получается другой прокси-объект, который вступит во владение задача обновления. Это кажется огромной ловушкой для любого, кто изменяет класс позже.

Я думаю, что было бы безопаснее, если бы complicated отслеживал количество живых proxy объектов, созданных на нем, чтобы последний уничтожаемый proxy выполнял вызов обновления.

0 голосов
/ 11 апреля 2019

Следуя Дмитрию Гордону аргументу людей, выбирающих "неправильный" подход, вы можете немного упростить вопрос (особенно с точки зрения пользователя):

class Demo
{
    int m_x
    int m_y; // cached value, result of complex calculation
    bool m_isDirtyY;
public:
    int x() { return m_x; }
    void x(int value) { m_x = value; m_isDirtyY = true; }
    int y()
    {
        if(m_isDirtyY)
        {
            // complex calculations...
            m_y = result;
            m_isDirtyY = false;
        }
        return m_y;
    }
};

ЭтоКстати, вы будете когда-либо выполнять вычисления только по необходимости, без дополнительных расширений, таких как прокси-объекты или явные транзакции.

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

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

...