C++ コア ガイドライン:制御構造の詳細

私の最後のドイツ語の投稿 C++ Core Guidelines:To Switch or not to Switch, それは多くの注目を集めた質問です. switch ステートメントの代わりにハッシュ テーブルを使用することは、非常に感情的なトピックのようです。なので当初の予定を変更。今日は、さまざまな種類の制御構造を紹介します。 if ステートメントと switch ステートメントから始めて、ハッシュ テーブルに進み、動的および静的ポリモーフィズムで終わります。さらに、パフォーマンスと保守性に関するいくつかの注意点を示します。

従来の制御構造は if ステートメントです。したがって、これが私の出発点です。

if ステートメント

これが、さまざまな制御構造で実装する簡単なプログラムです。

// dispatchIf.cpp

#include <chrono>
#include <iostream>

enum class MessageSeverity{ // (2)
 information,
 warning,
 fatal,
};

auto start = std::chrono::steady_clock::now(); // (4)

void writeElapsedTime(){ 
 auto now = std::chrono::steady_clock::now(); // (5)
 std::chrono::duration<double> diff = now - start;
 
 std::cerr << diff.count() << " sec. elapsed: ";
}

void writeInformation(){ std::cerr << "information" << std::endl; }
void writeWarning(){ std::cerr << "warning" << std::endl; }
void writeUnexpected(){ std::cerr << "unexpected" << std::endl; }

void writeMessage(MessageSeverity messServer){ // (1)
 
 writeElapsedTime(); // (3)
 
 if (MessageSeverity::information == messServer){
 writeInformation();
 }
 else if (MessageSeverity::warning == messServer){
 writeWarning();
 }
 else{
 writeUnexpected();
 }
 
}

int main(){

 std::cout << std::endl;
 
 writeMessage(MessageSeverity::information);
 writeMessage(MessageSeverity::warning);
 writeMessage(MessageSeverity::fatal);

 std::cout << std::endl;

}

行 (1) の関数 writeMessage は、プログラムの開始からの経過時間 (3) を秒単位で表示し、ログ メッセージを表示します。メッセージ重大度の列挙 (2) を使用します。開始時間 (4) と実際の時間 (5) を使用して、経過時間を計算します。名前が示すように、std::steady_clock は調整できません。したがって、この測定には正しい選択です。プログラムの重要な部分は、関数 writeMessage (1) の部分で、どのメッセージを表示するかを決定します。この場合、if-else ステートメントを使用しました。

それを正しくするために、if-else ステートメントの構文を調べる必要がありました。

プログラムの出力は次のとおりです。

残りの例の出力はスキップします。数字以外はいつも同じです。

switch ステートメント

次のプログラムは、前のプログラムとよく似ています。関数 writeMessage の実​​装のみが変更されました。

// dispatchSwitch.cpp

#include <chrono>
#include <iostream>

enum class MessageSeverity{
 information,
 warning,
 fatal,
};

auto start = std::chrono::steady_clock::now();

void writeElapsedTime(){
 auto now = std::chrono::steady_clock::now();
 std::chrono::duration<double> diff = now - start;
 
 std::cerr << diff.count() << " sec. elapsed: ";
}

void writeInformation(){ std::cerr << "information" << std::endl; }
void writeWarning(){ std::cerr << "warning" << std::endl; }
void writeUnexpected(){ std::cerr << "unexpected" << std::endl; }

void writeMessage(MessageSeverity messSever){
 
 writeElapsedTime();

 switch(messSever){
 case MessageSeverity::information:
 writeInformation();
 break;
 case MessageSeverity::warning:
 writeWarning();
 break;
 default:
 writeUnexpected();
 break;
 }
 
}

int main(){

 std::cout << std::endl;
 
 writeMessage(MessageSeverity::information);
 writeMessage(MessageSeverity::warning);
 writeMessage(MessageSeverity::fatal);

 std::cout << std::endl;

}

短くします。ハッシュ テーブルを続けましょう。

ハッシュテーブル

switch ステートメントとハッシュ テーブルの詳細な説明については、私の最後の投稿をお読みください:C++ コア ガイドライン:切り替えるかどうか、それが問題です。

// dispatchHashtable.cpp

#include <chrono>
#include <functional>
#include <iostream>
#include <unordered_map>

enum class MessageSeverity{
 information,
 warning,
 fatal,
};

auto start = std::chrono::steady_clock::now();

void writeElapsedTime(){
 auto now = std::chrono::steady_clock::now();
 std::chrono::duration<double> diff = now - start;
 
 std::cerr << diff.count() << " sec. elapsed: ";
}

void writeInformation(){ std::cerr << "information" << std::endl; }
void writeWarning(){ std::cerr << "warning" << std::endl; }
void writeUnexpected(){ std::cerr << "unexpected" << std::endl; }

std::unordered_map<MessageSeverity, std::function<void()>> mess2Func{
 {MessageSeverity::information, writeInformation},
 {MessageSeverity::warning, writeWarning},
 {MessageSeverity::fatal, writeUnexpected}
};

void writeMessage(MessageSeverity messServer){
 
 writeElapsedTime();
 
 mess2Func[messServer]();
 
}

int main(){

 std::cout << std::endl;
 
 writeMessage(MessageSeverity::information);
 writeMessage(MessageSeverity::warning);
 writeMessage(MessageSeverity::fatal);

 std::cout << std::endl;

}

終わりですか?いいえ? C++ には、私の読者の何人かが議論の中で言及した動的および静的ポリモーフィズムがあります。 if-else または switch ステートメントでは、適切なケースにディスパッチするために列挙子を使用しました。ハッシュ テーブルのキーも同様に動作します。

動的ポリモーフィズムと静的ポリモーフィズムはまったく異なります。適切なアクションをディスパッチするための列挙子またはキーの代わりに、実行時 (動的ポリモーフィズム) またはコンパイル時 (静的ポリモーフィズム) に何をすべきかを自律的に決定するオブジェクトを使用します。

動的ポリモーフィズムを続けましょう。

動的ポリモーフィズム

いいえ、決定ロジックは型階層にエンコードされています。

// dispatchDynamicPolymorphism.cpp

#include <chrono>
#include <iostream>

auto start = std::chrono::steady_clock::now();

void writeElapsedTime(){
 auto now = std::chrono::steady_clock::now();
 std::chrono::duration<double> diff = now - start;
 
 std::cerr << diff.count() << " sec. elapsed: ";
}

struct MessageSeverity{ // (1)
 virtual void writeMessage() const {
 std::cerr << "unexpected" << std::endl;
 }
};

struct MessageInformation: MessageSeverity{ // (2)
 void writeMessage() const override {
 std::cerr << "information" << std::endl;
 }
};

struct MessageWarning: MessageSeverity{ // (3)
 void writeMessage() const override {
 std::cerr << "warning" << std::endl;
 }
};

struct MessageFatal: MessageSeverity{};

void writeMessageReference(const MessageSeverity& messServer){
 
 writeElapsedTime();
 messServer.writeMessage();
 
}

void writeMessagePointer(const MessageSeverity* messServer){
 
 writeElapsedTime();
 messServer->writeMessage();
 
}

int main(){

 std::cout << std::endl;
 
 MessageInformation messInfo;
 MessageWarning messWarn;
 MessageFatal messFatal;
 
 MessageSeverity& messRef1 = messInfo; 
 MessageSeverity& messRef2 = messWarn;
 MessageSeverity& messRef3 = messFatal;
 
 writeMessageReference(messRef1); // (4)
 writeMessageReference(messRef2);
 writeMessageReference(messRef3);
 
 std::cerr << std::endl;
 
 MessageSeverity* messPoin1 = new MessageInformation;
 MessageSeverity* messPoin2 = new MessageWarning;
 MessageSeverity* messPoin3 = new MessageFatal;
 
 writeMessagePointer(messPoin1); // (5)
 writeMessagePointer(messPoin2);
 writeMessagePointer(messPoin3);
 
 std::cout << std::endl;

}

クラス (1)、(2)、および (3) は、使用する場合に何を表示する必要があるかを知っています。重要な考え方は、静的型 MessageSeverity は MessageInformation(4) などの動的型とは異なるということです。したがって、遅延バインディングが開始され、動的型の writeMessage メソッド (5)、(6)、および (7) が使用されます。動的ポリモーフィズムには一種の間接化が必要です。参照 (8) またはポインター (9) を使用できます。

パフォーマンスの観点からは、コンパイル時にディスパッチを行うことで改善できます。

静的ポリモーフィズム

静的ポリモーフィズムは、しばしば CRTP と呼ばれます。 CRTP は C++ イディオム C の略です 不思議なことに R 繰り返し T テンプレート P アターン。不思議なことに、クラスはそれ自体をテンプレート引数として使用するクラス テンプレートのインスタンス化からこの手法を派生させているためです。

// dispatchStaticPolymorphism.cpp

#include <chrono>
#include <iostream>

auto start = std::chrono::steady_clock::now();

void writeElapsedTime(){
 auto now = std::chrono::steady_clock::now();
 std::chrono::duration<double> diff = now - start;
 
 std::cerr << diff.count() << " sec. elapsed: ";
}

template <typename ConcreteMessage> // (1)
struct MessageSeverity{
 void writeMessage(){ // (2)
 static_cast<ConcreteMessage*>(this)->writeMessageImplementation();
 }
 void writeMessageImplementation() const {
 std::cerr << "unexpected" << std::endl;
 }
};

struct MessageInformation: MessageSeverity<MessageInformation>{
 void writeMessageImplementation() const { // (3)
 std::cerr << "information" << std::endl;
 }
};

struct MessageWarning: MessageSeverity<MessageWarning>{
 void writeMessageImplementation() const { // (4)
 std::cerr << "warning" << std::endl;
 }
};

struct MessageFatal: MessageSeverity<MessageFatal>{}; // (5)

template <typename T>
void writeMessage(T& messServer){ 
 
 writeElapsedTime(); 
 messServer.writeMessage(); // (6)
 
}

int main(){

 std::cout << std::endl;
 
 MessageInformation messInfo;
 writeMessage(messInfo);
 
 MessageWarning messWarn;
 writeMessage(messWarn);
 
 MessageFatal messFatal;
 writeMessage(messFatal);
 
 std::cout << std::endl;

}

この場合、すべての具象クラス (3)、(4)、および (5) は、基本クラス MessageSeverity から派生します。メソッド writeMessage は、具体的な実装 writeMessageImplementation にディスパッチする一種のインターフェースです。これを実現するために、オブジェクトは ConcreteMessage にアップキャストされます:static_cast(this)->writeMessageImplementation();。これはコンパイル時の静的ディスパッチです。したがって、この手法は静的ポリモーフィズムと呼ばれます。

正直、慣れるまで時間がかかりましたが、(6)行目の静的ポリモーフィズムの適用は至って簡単です。奇妙に繰り返されるテンプレート パターンにまだ興味がある場合は、それについての記事を書きました:C++ is Lazy:CRTP

比較の締めくくりとして、これらのさまざまな手法を比較してみましょう。

私の簡単な比較

最初に、制御構造を実装および維持するための好みの方法を見てみましょう。 C プログラマーとしての経験にもよりますが、if または switch ステートメントは非常に自然に思えます。インタープリターのバックグラウンドがある場合は、ハッシュ テーブルを好むかもしれません。オブジェクト指向の背景があるため、制御構造を実装するには動的ポリモーフィズムが好まれます。 CRTP とも呼ばれる静的ポリモーフィズムは非常に特殊です。したがって、慣れるまでに時間がかかります。その後、それはあなたが使用しなければならないかなりのパターンです.

セキュリティの観点から、新しいコンテキスト依存識別子のオーバーライドについて言及する必要があります。型階層で仮想メソッドをオーバーライドする意図を表現するのに役立ちます。間違えると、コンパイラは文句を言います。

次に、より興味深い質問に進みます。パフォーマンスの違いは何ですか?数値なしで大まかなアイデアのみを提供します。長い一連の if ステートメントがある場合、多くの比較が含まれるため、非常にコストがかかります。動的ポリモーフィズムとハッシュ テーブルはより高速であり、どちらの場合もポインターの間接化が関係するため、同じ球場に収まります。 switch ステートメントと静的ポリモーフィズムは、コンパイル時に決定を下します。したがって、これらは 2 つの最速の制御構造です。

次は?

さまざまな制御構造の説明が終わったことを願っています。したがって、次の投稿ではステートメントの最後の規則を説明し、算術式の規則から始めます。