使用代理对象延迟更新与“避免使用自定义构造和销毁的未命名对象”

问题描述 投票:2回答:3

我有一个类complicated,其中包含修改某些内部状态的各种setter。内部状态修改可能很昂贵,所以我不想经常这样做。特别是,如果立即连续调用几个setter,我想在最后一个setter调用之后只执行一次内部状态的昂贵更新。

我用代理解决了(或“解决”?)这个要求。以下是最小的工作代码示例:

#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 ++实现的目标?

c++ c++11 c++14 c++17
3个回答
3
投票

我认为你的解决方案是合法的,但它有一个缺点,它隐藏在代码的用户,更新是昂贵的,所以更可能写:

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

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

我建议将setter私有并添加一个朋友事务类,以便修改对象看起来像:

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的唯一方法 - 用户将更倾向于在一次交易中调用几个setter。


2
投票

我在这里看到的危险是你的班级有任何方法不返回代理(或任何公共成员)。如果使用代理的operator->(产生complicated),则禁用更新调用,但只有在使用operator->总是产生另一个将接管更新任务的代理对象时,这才是安全的。对于后来修改课程的人来说,这似乎是一个巨大的陷阱。

我认为如果complicated跟踪在其上创建的活着的proxy对象的数量将更安全,以便要销毁的最后一个proxy执行更新调用。


0
投票

根据Dmitry Gordon关于人们选择“错误”方法的论点,你可能会有更简单的事情(特别是从用户的角度来看):

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

这样,您只需要根据需要执行计算,不需要像代理对象或显式事务那样的额外扩展。

根据您的需要,您可能将此模式封装在单独的(模板?)类中,可能接收更新程序对象(lambda?)或使用纯虚拟update函数来重复更少的代码。

附注:设置一个值可能会使一个以上的缓存值无效 - 没问题,将多个脏标志设置为true然后......

© www.soinside.com 2019 - 2024. All rights reserved.