Is Object Editor a good approach if there are multiple member functions to call?

妖精的绣舞 提交于 2019-12-11 02:28:46

问题


I am often annoyed by sequential calls of class member function like this (ignore new usage, it's for Qt, but it's not strictly Qt-related)

A a = new A();
a->fun1("one");
a->fun2(1, 2);
...
a->fun10("end");

I always felt that such code should be written as simple instruction, not a project dominating lines.

Simple example from Qt:

auto* spinBox = new QSpinBox();
spinBox->setRange(-100, 100);
spinBox->setValue(50);
spinBox->setSingleStep(5);
newLayout->addWidget(spinBox);

But I would prefer to do this in simple single line instead of that. So I have written something like this:

class {
public:
    template<class X>
    auto& operator()(X* ptr) {
        this->ptr = ptr;
        return *this;
    }

    template<class X, class R, class... Args>
    auto& operator()(R (X::* fun)(Args...), Args... args) {
        if(ptr == nullptr) {
            std::cerr << "Editor can't edit nullptr" << std::endl;
            return *this;
        }
        auto call = std::mem_fn(fun);
        call(*static_cast<X*>(ptr), args...);
        return *this;
    }

    template <class X>
    operator X*() {
        auto* result = static_cast<X*>(ptr);
        ptr = nullptr;
        return result;
    }
private:
    void *ptr = nullptr;
} EDITOR;

And now the usage:

newLayout->addWidget(EDITOR(new QSpinBox)(&QSpinBox::setRange,-100, 100)(&QSpinBox::setValue, 50)(&QSpinBox::setSingleStep, 5));

Is it good approach, except not being type-safe? (I could live with that)

--- EDIT ---

Another, type-safe approach would be:

template<class X>
class EDITOR2 {
public:
    EDITOR2(X* ptr) {
        this->ptr = ptr;
    }

    template<class R, class... Args>
    auto& operator()(R (X::* fun)(Args...), Args&&... args) {
        if(ptr == nullptr) {
            std::cerr << "Editor can't edit nullptr";
            return *this;
        }
        auto call = std::mem_fn(fun);
        call(*ptr, args...);
        return *this;
    }

    operator X*() {
        return ptr;
    }

    X *ptr;
};

With usage:

newLayout->addWidget(EDITOR2<QSpinBox>(new QSpinBox)(&QSpinBox::setRange, -100, 100)(&QSpinBox::setValue, 50)(&QSpinBox::setSingleStep, 5));

But that requires to recreate editor object every time, and adds additional usage code.


回答1:


This is kind of like a fluent interface, except rather than having a bunch of named functions to as a builder, you just use pointers to members. It's a reasonable approach, if you're into that sort of thing (primarily opinion based), but the total lack of type safety is not ok.

Regardless std::cerr is not a good way to do error handling. throw or assert.

You can improve it a lot:

template <class T>
struct EditorImpl
{
    T* ptr;

    template <class F, class... Args>
    Editor& operator()(F&& f, Args&&... args)
    {
        std::invoke(std::forward<F>(f), ptr, std::forward<Args>(args)...);
        return *this;
    }

    T* yield() const {
        return ptr;
    }
};

template <class T>
EditorImpl<T> Editor(T* ptr) { return EditorImpl<T>{ptr}; }

And then you can write:

newLayout->addWidget(
    Editor(new QSpinBox)
    (&QSpinBox::setRange,-100, 100)
    (&QSpinBox::setValue, 50)
    (&QSpinBox::setSingleStep, 5)
    .yield());

Though this would probably work better if the interface was already fluent:

newLayout->addWidget(
    (new QSpinBox)
    ->range(-100, 100)
    ->value(50)
    ->singleStep(5));

But that would imply writing a bunch of new named functions, which you're definitely (probably?) not going to do.


std::invoke() is C++17, but is implementable in C++11.




回答2:


Let's fight,

First opponent, vanilla approach:

auto* spinBox = new QSpinBox();
spinBox->setRange(-100, 100);
spinBox->setValue(50);
spinBox->setSingleStep(5);
newLayout->addWidget(spinBox);
  • Pros:

    • It's the way all people do this
    • It's pretty readable being ~30 columns wide
  • Cons:

    • It takes 5 lines
    • The subject of the action spinBox is repeated each time

Second opponent, fancy approach:

newLayout->addWidget(EDITOR2<QSpinBox>(new QSpinBox)(&QSpinBox::setRange, -100, 100)(&QSpinBox::setValue, 50)(&QSpinBox::setSingleStep, 5));
  • Pros:

    • It takes one line
    • It is somewhat a more functional programming approach
  • Cons:

    • It's a pretty long line that's hard to read
    • By default people's reaction will be a variant of wtf since they are not used to it
    • The instance name spinBox is not repeated but the class name QSpinBox now is

The final choice is yours based on how much you value each point, i did compare the second approach in one line as if you use line breaks you're essentially getting back to the original thing you were trying to fix

In my humble opinion adding a class overhead for so little is not worth it, and i am the kind of person annoyed by the cons of the original approach, the killer point being the readability loss which will most likely force you to use line breaks which in turns pretty much means you've done all this for nothing.



来源:https://stackoverflow.com/questions/42140222/is-object-editor-a-good-approach-if-there-are-multiple-member-functions-to-call

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!