C++ コア ガイドライン:ソース ファイル

ソース ファイルの構成は、C++ ではめったに取り上げられないトピックです。 C++20 ではモジュールを取得しますが、それまではコードの実装とインターフェースを区別する必要があります。

C++ コア ガイドラインでは、ソース ファイルについて次のように明確に説明しています。したがって、ソース ファイルには 10 以上のルールがあります。最初の 11 の規則はインターフェース ファイル (*.h ファイル) と実装ファイル (*.cpp ファイル) を扱い、最後の 3 つは名前空間を扱います。

インターフェイスと実装ファイルのルールから始めましょう。最初の 7 つは次のとおりです。

  • SF.1:.cpp を使用する コードファイルのサフィックスと .h プロジェクトが別の規則に従っていない場合のインターフェイス ファイル用
  • SF.2:.h ファイルにオブジェクト定義または非インライン関数定義を含めることはできません
  • SF.3:.h を使用 複数のソース ファイルで使用されるすべての宣言のファイル
  • SF.4:.h を含める ファイル内の他の宣言の前のファイル
  • SF.5:.cpp ファイルには .h を含める必要があります インターフェイスを定義するファイル
  • SF.6:using namespace を使用 基盤ライブラリ用の移行用ディレクティブ (std など) )、またはローカル スコープ内 (のみ)
  • SF.7:using namespace と書かないでください ヘッダー ファイルのグローバル スコープ

各ルールについて詳しくは書きませんが、ルールを引用するだけで、最初のルールから読みやすいストーリーを作成したいと思います.

わかりました、SF.1:.cpp を使用してください コードファイルのサフィックスと .h プロジェクトが一貫性に関する別の規則にまだ従っていない場合は、インターフェイス ファイル用です。 C++ プロジェクトがある場合、ヘッダー ファイルの名前は *.h で、実装ファイルの名前は *.cpp にする必要があります。私たちのプロジェクトにすでに別のポリシーがある場合、規則はこのルールよりも優先されます。

もちろん、ヘッダーと実装ファイルの他の規則もよく見ました。以下に、私が心に留めているいくつかを示します。

  • ヘッダー ファイル:
    • *.h
    • *.hpp
    • *.hxx
  • 実装ファイル:
    • *.cpp
    • *.c
    • *.cc
    • *.cxx

他のさまざまな規則を知っていると思います.

ヘッダー ファイルにオブジェクト定義または非インライン関数の定義が含まれている場合、リンカーがエラーを出すことがあります。これが 2 番目の規則 SF.2 の理由です:A .h ファイルには、オブジェクト定義または非インライン関数定義を含めることはできません。より具体的に言うと、C++ には 1 つの定義規則があります:

ODR

ODR は One Definition Rule の略で、関数の場合に言います。

  • 関数は、どの翻訳単位でも複数の定義を持つことはできません。
  • 関数は、プログラム内で複数の定義を持つことはできません。
  • 外部リンケージを持つインライン関数は、複数の翻訳で定義できます。定義は、各定義が同じでなければならないという要件を満たす必要があります。

最近のコンパイラでは、キーワード inline は関数のインライン化に関するものではなくなりました。最近のコンパイラは、それをほぼ完全に無視しています。多かれ少なかれインラインの使用例は、関数に ODR の正確性をマークすることです。私の意見では、インラインという名前は最近ではかなり誤解を招くものです。

1 つの定義規則に違反するプログラムをリンクしようとしたときに、リンカーが何を言わなければならないかを見てみましょう。次のコード例には、1 つのヘッダー ファイル header.h と 2 つの実装ファイルがあります。実装ファイルにはヘッダー ファイルが含まれているため、func 出口の定義が 2 つあるため、1 つの定義ルールが破られます。

// header.h

void func(){}

// impl.cpp

#include "header.h"

// main.cpp

#include "header.h"

int main(){}

リンカーは、func の複数の定義について不平を言っています:

次の 2 つのルールは、可読性と保守性の観点から明らかです:SF.3:.h を使用する 複数のソース ファイルと SF.4 で使用されるすべての宣言のファイル:.h を含める ファイル内の他の宣言の前。

ルール 5 はより興味深い:SF.5:A .cpp ファイルには .h を含める必要があります インターフェイスを定義するファイル。興味深い質問は、*.h ファイルを *.cpp ファイルに含めず、インターフェース ファイル *.h と実装ファイル *.cpp の間に不一致があるとどうなるかということです。

悪い日だったとします。 int を取得して int を返す関数 func を定義しました。

// impl.cpp

// #include "impl.h" 

int func(int){
 return 5;
}

私の間違いは、ヘッダー ファイル impl.h でこの関数を宣言して int を取得し、std::string を返したことです。

// impl.h

#include <string>

std::string func(int);

この関数をメイン プログラムで使用したいので、ヘッダーをメイン プログラムに含めます。

// main.cpp

#include "impl.h"

int main(){
 
 auto res = func(5);
 
}

問題は、メイン プログラム main.cpp がコンパイルされるリンク時までエラーが遅延する可能性があることです。これでは遅すぎます。

impl.cpp ファイルにヘッダー impl.h を含めると、コンパイル時にエラーが発生します。

次のルールは名前空間に関するものです:SF.6:using namespace を使用する 移行のためのディレクティブ、基盤ライブラリ用 (std など) )、またはローカル スコープ内 (のみ)。正直なところ、このルールは私には弱すぎます。次の例のような名前空間ディレクティブの使用には反対です。

#include <cmath>
using namespace std;

int g(int x)
{
 int sqrt = 7;
 // ...
 return sqrt(x); // error
}

名前の衝突があるため、プログラムはコンパイルされません。これは、ディレクティブの使用に対する私の主な議論ではありません。私の主な主張は、using ディレクティブが名前の由来を隠し、コードの可読性を損なうというものです。

#include <iostream>
#include <chrono>

using namespace std;
using namespace std::chrono;
using namespace std::literals::chrono_literals;

int main(){

 std::cout << std::endl;

 auto schoolHour= 45min;

 auto shortBreak= 300s;
 auto longBreak= 0.25h;

 auto schoolWay= 15min;
 auto homework= 2h;

 auto schoolDayInSeconds= 2 * schoolWay + 6 * schoolHour + 4 * shortBreak + longBreak + homework;

 cout << "School day in seconds: " << schoolDayInSeconds.count() << endl;

 duration<double, ratio<3600>> schoolDayInHours = schoolDayInSeconds;
 duration<double, ratio<60>> schoolDayInMinutes = schoolDayInSeconds;
 duration<double, ratio<1, 1000>> schoolDayInMilliseconds = schoolDayInSeconds;

 cout << "School day in hours: " << schoolDayInHours.count() << endl;
 cout << "School day in minutes: " << schoolDayInMinutes.count() << endl;
 cout << "School day in milliseconds: " << schoolDayInMilliseconds.count() << endl;

 cout << endl;

}

どの名前空間でどのリテラル、関数、またはオブジェクトが定義されているか、覚えていますか?そうでない場合、名前の定義を探すのが困難になる可能性があります。これは特に、初心者の場合に当てはまります。

この投稿を終了する前に、言及しなければならないインポート ルールが 1 つあります:SF.7:using namespace を書き込まないでください ヘッダー ファイルのグローバル スコープで。根拠は次のとおりです。

ヘッダーのグローバル スコープの using 名前空間は、そのヘッダーを含むすべてのファイルに名前を挿入します。これにはいくつかの結果があります:

  • ヘッダーを使用する場合、using ディレクティブを元に戻すことはできません。
  • 名前衝突の危険性が大幅に高まります。
  • 含まれている名前空間を変更すると、新しい名前が導入されたため、ビルドが壊れる可能性があります。

次は?

まず、ソース ファイルの編成にはいくつかの規則が残されています。さらに、C++20 のモジュールを取得します。これらの重要な機能が C++ に与える影響を見てみましょう-


No