Лучший способ использования именованного параметра C++?
Я занимаюсь разработкой библиотеки графического интерфейса для Windows (как личный побочный проект, без надежд на полезность). Для моего класса главного окна я настроил иерархию классов параметров (используя идиом именованных параметров), потому что некоторые параметры являются общими, а другие специфичны для определенных типов окон (например, диалогов).
Как работает именованный идиоматический параметр, функции класса параметров должны возвращать объект, для которого они вызваны. Проблема в том, что в иерархии каждый должен быть отдельным классом - createWindowOpts
класс для стандартных окон createDialogOpts
класс для диалогов и тому подобное. Я справился с этим, сделав все шаблоны классов опций. Вот пример:
template <class T>
class _sharedWindowOpts: public detail::_baseCreateWindowOpts {
public: ///////////////////////////////////////////////////////////////
// No required parameters in this case.
_sharedWindowOpts() { };
typedef T optType;
// Commonly used options
optType& at(int x, int y) { mX=x; mY=y; return static_cast<optType&>(*this); }; // Where to put the upper-left corner of the window; if not specified, the system sets it to a default position
optType& at(int x, int y, int width, int height) { mX=x; mY=y; mWidth=width; mHeight=height; return static_cast<optType&>(*this); }; // Sets the position and size of the window in a single call
optType& background(HBRUSH b) { mBackground=b; return static_cast<optType&>(*this); }; // Sets the default background to this brush
optType& background(INT_PTR b) { mBackground=HBRUSH(b+1); return static_cast<optType&>(*this); }; // Sets the default background to one of the COLOR_* colors; defaults to COLOR_WINDOW
optType& cursor(HCURSOR c) { mCursor=c; return static_cast<optType&>(*this); }; // Sets the default mouse cursor for this window; defaults to the standard arrow
optType& hidden() { mStyle&=~WS_VISIBLE; return static_cast<optType&>(*this); }; // Windows are visible by default
optType& icon(HICON iconLarge, HICON iconSmall=0) { mIcon=iconLarge; mSmallIcon=iconSmall; return static_cast<optType&>(*this); }; // Specifies the icon, and optionally a small icon
// ...Many others removed...
};
template <class T>
class _createWindowOpts: public _sharedWindowOpts<T> {
public: ///////////////////////////////////////////////////////////////
_createWindowOpts() { };
// These can't be used with child windows, or aren't needed
optType& menu(HMENU m) { mMenuOrId=m; return static_cast<optType&>(*this); }; // Gives the window a menu
optType& owner(HWND hwnd) { mParentOrOwner=hwnd; return static_cast<optType&>(*this); }; // Sets the optional parent/owner
};
class createWindowOpts: public _createWindowOpts<createWindowOpts> {
public: ///////////////////////////////////////////////////////////////
createWindowOpts() { };
};
Это работает, но, как вы можете видеть, это требует значительного объема дополнительной работы: приведение типов к типу возвращаемого значения для каждой функции, дополнительные классы шаблонов и так далее.
У меня вопрос: есть ли в этом случае более простой способ реализовать идиому именованного параметра, который не требует дополнительных затрат?
6 ответов
Может быть, не то, что вы хотите услышать, но я думаю, что вполне нормально иметь много уродливых приведений типов и параметров шаблона в библиотечном коде, который (более или менее) скрыт от клиента, если он безопасен и делает жизнь клиента намного проще. Прелесть в библиотечном коде не в самом коде, а в коде, который позволяет клиентам писать. Возьмите STL для примера.
Я также разработал небольшую GUI-библиотеку в качестве личного проекта с теми же стремлениями, что и у вас, и часть кода становится в ней довольно уродливой, но в итоге она позволяет мне писать красивый клиентский код (по крайней мере, в моем (возможно извращенные) глаза) и вот что ИМХО считается.
Как насчет...?
template <class T>
class _sharedWindowOpts: public detail::_baseCreateWindowOpts {
protected: // (protected so the inheriting classes may also use it)
T & me() { return static_cast<T&>(*this); } // !
public:
// No required parameters in this case.
_sharedWindowOpts() { };
typedef T optType;
// Commonly used options
optType& at(int x, int y) { mX=x; mY=y; return me(); }; // !
// ...
};
Не могли бы вы просто связать вызовы методов в обратном порядке наследования?
Так что в вашем примере вы бы сделали что-то вроде
Window window = CreateWindow("foo"). Menu(hmenu).owner(hwnd).at(0,0).background(hbr);
Я понимаю, что это не на 100% прозрачно, но кажется немного легче и почти правильно.
Я не знаю, влюблен ли я в этот ответ, но есть возможность с использованием вывода аргументов шаблона. ПРИМЕЧАНИЕ. У меня нет моего компилятора, я перепроверю его завтра, если кто-то еще не захочет его обернуть.
class sharedWindowOpts
{
public:
sharedWindowOpts() {};
// Commonly used options
template <class optType>
static optType& at(int x, int y, optType& opts) { opts.mX=x; opts.mY=y; return opts; };
template <class optType>
static optType& background(HBRUSH b, optType& opts) { opts.mBackground=b; return opts; };
// etc...
}
class createWindowOpts : public sharedWindowOpts
{
public:
createWindowOpts() : sharedwindowOpts() {};
// These can't be used with child windows, or aren't needed
template <class optType>
static optType& menu(HMENU m, optType& opts) { opts.mMenuOrId=m; return opts; };
template <class optType>
static optType& owner(HWND hwnd, optType& opts) { opts.mParentOrOwner=hwnd; return opts; };
}
Тогда вы бы назвали CreateWindow следующим образом:
CreateWindow( createWindowOpts::owner(hwnd,
createWindowOpts::at(0, 100, // can use createWindowOpts because it doesn't hide sharedWindowsOpts::at
createWindowOpts::menu(hmenu, createWindowOpts() ) ) ) );
Конечно, неприятные вещи в этом случае заключаются в использовании статического метода, вызывающего синтаксис и все лишние скобки. Если вы замените статические функции-члены функциями, не являющимися членами, это можно устранить. Тем не менее, он избегает приведения типов и дополнительных шаблонных классов.
Лично я предпочел бы иметь нечетный код в библиотеке, как с вашим методом, чем везде, где библиотека используется, как у меня.
Шаблоны горячие.
Но POP (Простой старый полиморфизм) не умер.
Почему бы не вернуть (умный) указатель на подкласс?
Я знаю, что опаздываю на год и у меня не хватает доллара, но я все равно предложу свое решение.
//////// Base..
template<typename DerivedBuilder, typename Options>
class Builder
{
protected:
Builder() {}
DerivedBuilder& me() { return *static_cast<DerivedBuilder*>(this); }
Options options;
};
////////////////////////// A //////////////////////////
class Options_A
{
public:
Options_A() : a(7) {}
int a;
};
class Builder_A;
class A
{
public:
virtual ~A() {}
virtual void print() { cout << "Class A, a:" << a << endl; }
protected:
friend class Builder_A;
A(const Options_A& options) : a(options.a) {}
int a;
};
template<typename DerivedBuilder, typename Options = Options_A>
class BuilderT_A : public Builder<DerivedBuilder, Options>
{
public:
using Builder<DerivedBuilder, Options>::options;
using Builder<DerivedBuilder, Options>::me;
DerivedBuilder& a(int p) { options.a = p; return me(); }
};
class Builder_A : public BuilderT_A<Builder_A>
{
public:
shared_ptr<A> create()
{
shared_ptr<A> obj(new A(options));
return obj;
}
};
////////////////////////// B //////////////////////////
class Options_B : public Options_A
{
public:
Options_B() : b(8) {}
int b;
};
class Builder_B;
class B : public A
{
public:
virtual ~B() {}
virtual void print() { cout << "Class B, a:" << a << ", b:" << b << endl; }
protected:
friend class Builder_B;
B(const Options_B& options) : A(options), b(options.b) {}
int b;
};
template<typename DerivedBuilder, typename Options = Options_B>
class BuilderT_B : public BuilderT_A<DerivedBuilder, Options>
{
public:
using Builder<DerivedBuilder, Options>::options;
using Builder<DerivedBuilder, Options>::me;
DerivedBuilder& b(int p) { options.b = p; return me(); }
};
class Builder_B : public BuilderT_B<Builder_B>
{
public:
shared_ptr<B> create()
{
shared_ptr<B> obj(new B(options));
return obj;
}
};
////////////////////////// C //////////////////////////
class Options_C : public Options_B
{
public:
Options_C() : c(9) {}
int c;
};
class Builder_C;
class C : public B
{
public:
virtual ~C() {}
virtual void print() { cout << "Class C, a:" << a << ", b:" << b << ", c:" << c << endl; }
protected:
friend class Builder_C;
C(const Options_C& options) : B(options), c(options.c) {}
int c;
};
template<typename DerivedBuilder, typename Options = Options_C>
class BuilderT_C : public BuilderT_B<DerivedBuilder, Options_C>
{
public:
using Builder<DerivedBuilder, Options>::options;
using Builder<DerivedBuilder, Options>::me;
DerivedBuilder& c(int p) { options.c = p; return *static_cast<DerivedBuilder*>(this); }
};
class Builder_C : public BuilderT_C<Builder_C>
{
public:
shared_ptr<C> create()
{
shared_ptr<C> obj(new C(options));
return obj;
}
};
///////////////////////////////////////////////////////////////////////////
int main()
{
shared_ptr<A> a = Builder_A().a(55).a(1).create();
a->print();
shared_ptr<B> b = Builder_B().b(99).b(2).a(88).b(4).a(2).b(3).create();
b->print();
shared_ptr<C> c = Builder_C().a(99).b(98).c(97).a(96).c(6).b(5).a(4).create();
c->print();
return 0;
}
/* Output:
Class A, a:1
Class B, a:2, b:3
Class C, a:4, b:5, c:6
*/
C выводится из B, а B выводится из A. Я повторил параметры, чтобы показать, что они могут располагаться в любом порядке.