web-dev-qa-db-ja.com

これは、C ++のパターンに適合するクラスを定義するためにマクロを使用することは健全な考えですか?

私はすべて異なる機能(「オペレーター」のグループのようなもの)を担当する基本クラスから継承する一連のクラスを持っています。これらはすべて同じ入力で動作し、同じ出力タイプを出力しますが、異なる操作は他の状態で内部的に実行されます。 JSONにシリアル化し、UIから因数分解する必要があります

私の要件は、人々がそれを理解するのに苦労することなくこれに取り組むことができる必要があることです。他の誰かがこれを他の人と異なるプロジェクトで採用していて、これが経験から悪い考えであるかどうかと理由を教えてくれることを願っていました

これを行うために、クラス名をコンストラクターとメソッドに関連付け、クラスをシリアル化するマップを使用しました。最初に、これらのマップを格納する別のファイルにクラス情報を手動で入力していたところ、 静的クラスオブジェクトを使用してこれを行う方法 が見つかりました。これはどのクラスでも必要であり、使用するクラスの名前を除いてまったく同じ形式だったので、これを支援するために2つのマクロを作成しました(クラス内でMACRO_TAG_CLASS(classname)を実行するだけで済みます)宣言とMACRO_REGISTER_CLASS(classname)宣言後)。

これで、シリアル化とGUI表示を行う方法をリファクタリングするようになりました。このタイプの新しいクラスを作成する人の労力が減り、現在の使用方法に合わせて、より汎用的になっています。結局、すべてのクラスで同じようにQTプロパティを使用する必要があったので、これも処理するマクロを作成することに決めました(私が支援するためにブーストプリプロセッサを使用していることに注意してください)

これで、次のようなクラスができました。

class MyClass :
        public AbstractBase {
Q_OBJECT
private:
    TypeA m_foo;
    TypeB m_bar;
public:
    MACRO_TAG_CLASS(MyClass)

MACRO_ADD_PROPERTIES((TypeA, foo), (TypeB, bar))
//adds qt properties, signals, and adds the strings of the properties to a list for the class

   //other class functions...
   //constructor that is different per class

   //function every class in this heirarchy has, sort of operator(), 
   //but class is doesn't correspond 1:1 with the concept of an operator, so not a functor.
    AbstractBase *baz(X *x) override;

    virtual ~MyClass() = default;
};
MACRO_REGISTER_CLASS(MyClass)

そして私はそれらを次のようなものに変更することを考えていました:

#define START_CLASS_OF_BASE(CLASS_NAME, ...)\
class CLASS_NAME : public AbstractBase { \
Q_OBJECT \
 public: \
    MACRO_TAG_CLASS(CLASS_NAME) \
MACRO_ADD_PROPERTIES(__VA_ARGS__) \
private:

#define END_CLASS_OF_BASE(CLASS_NAME) \
}; \
    MACRO_REGISTER_CLASS(CLASS_NAME)

START_CLASS_OF_BASE(MyClass, (TypeA, foo), (TypeB, bar))
private:
    TypeA m_foo;
    TypeB m_bar;
public:
   //other class functions...
   //constructor that is different per class

    AbstractBase *baz(X *x) override;

    virtual ~MyClass() = default;
END_CLASS_OF_BASE(MyClass)

このパターンはこの特定のクラス階層にのみ表示されることに注意してください。他の人は最終的にこのコードを保守する必要があり、このパターンに適合するクラスは約20あります。

6
whn

ここでは、通常のクラスの代わりにマクロを使用することに反対しています。

  1. 特にコンパイルエラーがある場合は、デバッグが難しくなります。

  2. 新しいクラスを簡単に作成するコストが後で維持するのが難しい場合は、クラスを作成するためにより多くの作業を行うことを好みますが、後で維持する方が簡単です。

  3. この場合、金の見積もりが適用されると思います。「通常、コードの読み取りには、コードの作成よりも時間がかかります」

8
ocomfd

私は現在、これらのタイプのマクロで作成されたクラスを持つコードベースで作業しています。マクロ化されたコードのいずれかで将来何か問題が発生した場合、デバッグすることが不可能であるため、私はこの方法で物事を行うことを強くお勧めしません。どうしてもクラスを変えるのは非常に難しいです。そして、この方法で作成された特定のクラスが「1つだけ異なる」必要があるワンオフやEdgeのケースを見つけると、マクロはおそらく手動で拡張されるでしょう。その後、マクロへの将来の変更は、その拡張されたコピーも更新することを忘れない限り、その1つのクラスに影響を与えません。代わりにこれにC++テンプレートを使用できる方法がある場合は、それが望ましいでしょう。 (または、コメンターが示唆するように、これを行う方法を知っているライブラリーを使用します。)

11
user1118321

他の人がすでに指摘しているように、これにマクロを使用することはメンテナンスの恐怖になります。別のアプローチとして、繰り返しのボイラープレートコードを作成するための小さなコードジェネレーターを実装できます。

もちろん、コードジェネレーターはマクロと同様に維持する必要がありますが、主な利点は、それが(うまくいけば)独自にコンパイルされる読み取り可能なコードを生成し、(干渉するマクロコードなしで)独自にデバッグできることです。必要に応じて、個別に拡張します。

このようなコードジェネレーターの場合、2つのアプローチが可能です。単純な1回の初期生成と、その後の生成コードの手動メンテナンス、または再生成のサポート(生成コードと手動コードの分離に注意する必要がある)のいずれかです。どちらのアプローチもあなたのケースに適しているかもしれません、あなたは何が最もよく合うかを決定しなければなりません。

これは通常、将来クラスの数が増えるときに効果があり、損益分岐点は、要件の複雑さやジェネレーターの使用頻度に応じて、5から50のクラスになる可能性があります。

7
Doc Brown
class MyClass :
        public AbstractBase {
Q_OBJECT

ここではC++コードではなく、Qtコードを記述しています。批判的には、これはQt mocコンパイラによって最初に処理されるコードです。クラスでQ_OBJECTマクロを参照することを想定しており、そのクラスのメタデータを生成します。

だからあなたのマクロアプローチを見てみましょう:

class CLASS_NAME : public AbstractBase { \
Q_OBJECT \

mocQ_OBJECTを見つけたとしても、CLASS_NAMEのメタデータを生成します。もちろん、これはクラス名ではなく、マクロパラメータです。問題は、mocがすべてのマクロのインスタンス化を理解できないため、メタデータを生成できないことです。

ここでの基本的な問題は、QtプリプロセッサとCプリプロセッサの両方を使用して、段階的なコンパイルモデルで作業していることです。 C++の実現の1つは、Cプリプロセッサがソフトウェアエンジニアリングの観点から不便だったことです。そのため、C++テンプレートは言語に不可欠です。

ただし、これはQtや独自のプリプロセッサハックには役立ちません。 mocが最初に実行されるという事実に耐えなければなりません。

1
MSalters