问题
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
spinBoxis 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
wtfsince they are not used to it - The instance name
spinBoxis not repeated but the class nameQSpinBoxnow 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