TransWikia.com

c++での分割コンパイル

スタック・オーバーフロー Asked by TsuruharaKota on October 17, 2020

test2.hに宣言されている名前空間Bでtest1.hに宣言されている名前空間Aに宣言されたクラスをメンバとしているのですが, 以下のようなエラーが出ます.

test1.hでの #include"test2.h" を消せば上手く行くのは分かったのですが, なぜ上手く行くのかが分からないです. よろしくお願いします.

エラーメッセージ

    test2.h test1.h
In file included from test1.h:3:0,
                 from test1.cpp:1,
                 from test3.cpp:2:
test2.h:7:5: error: ‘A’ does not name a type
     A::K t;
     ^
In file included from test3.cpp:3:0:
test2.cpp: In function ‘void B::Func()’:
test2.cpp:3:2: error: ‘t’ was not declared in this scope
  t.k=100;
  ^
test2.cpp:3:2: note: suggested alternative: ‘tm’
  t.k=100;
  ^
  tm
In file included from test1.h:3:0:
test2.h:7:5: error: ‘A’ does not name a type
     A::K t;

ソースコード

test1.h

#ifndef test1_H_
#define test1_H_
#include<iostream>
#include"test2.h"
namespace A{
 class K{
  public:
  int k;
  void main();
 };
};
#endif

test1.cpp

#include"test1.h"
void A::K::main(){
 std::cout << A::K::k << std::endl;
}

test2.h

#ifndef test2_H_
#define test2_H_
#include<iostream>
#include"test1.h"
namespace B{
 A::K t;
 void Func();
};
#endif

test2.cpp

#include"test2.h"
void B::Func(){
 t.k=100;
 t.main();
}

test3.cpp

#include<iostream>
#include"test1.cpp"
#include"test1.h"
#include"test2.cpp"
#include"test2.h"
int main(){
 B::Func();
}

2 Answers

うまく行かない理由は、先に回答されている方の書かれた通りなのですが、では、どう書くのが正しいのか、について書いてみたいと思います。

ヘッダファイル test1.h と test2.h のように複数のヘッダファイルを作っていく場合には、依存関係が単一方向になるように設計する必要があります。

test1.h の先頭で test2.h をincludeするのであれば、test2.hの方ではtest1.hをincludeしてはいけません。逆もしかりです。

今回のヘッダファイルの内容を見ると、test2.hの記載内容をコンパイラが理解するためにはtest1.hの内容が必要ですが、test1.hをコンパイルするためにはtest2.hの内容をコンパイラが先に見ておく必要はありません。

ですので、test2.hからtest1.hをインクルードするようにします。

次にソースファイルですが、ソースファイルからソースファイルをインクルードすることはしません。

この例題ではソースファイルがtest1.cppと test2.cppとtest3.cppの3つしか登場しませんが、実際のプロジェクトではソースファイルが何千とか何万というのはざらです。そのような状況では1つ1つのソースファイルを別々にコンパイルします。
そのようにして何がうれしいかというと、一部のソースに加筆や修正を行ったときに、修正した一部のソースファイルだけコンパイルすることが可能になります。

今回のtest3.cppのように中心となる1つのソースファイルから、残りのソースファイルを全部includeしてしまっては、分割した意味がなくなってしまいます。

ですので、test3.cppから他のソースをincludeするのはやめて、test1.cpp、test2.cpp、test3.cppはそれぞれ独立にコンパイルして、最後にリンクします。

コンパイル操作はたとえば次のようにします。

$ g++ -c test1.cpp
$ g++ -c test2.cpp
$ g++ -c test3.cpp
$ g++ -o test123 test1.o test2.o test3.o

こんなのを毎回手で打っていてはたまらないのでツールで自動化します。

test1.cppのソースファイルをコンパイルするのに必要なヘッダは test1.h だけですので、test1.cppからは test1.h だけをincludeします。必要ないものまでincludeするとコンパイル時間がそれだけ余計にかかります。

test2.cpp, test3.cpp についても同様です。

ヘッダもソースも、必要最小限のヘッダファイルをincludeするように設計します。

追記

ときには、2つのヘッダに、お互いをincludeさせたくなることがあります。

典型的には、2つの構造体(struct)またはクラスが、互いを参照しているようなケースです。

たとえば、グラフ構造を表現するプログラムを書いているときに、節点(Node)と枝(Edge)のクラスがあって、Nodeの定義の中にEdgeを登場させたい、一方でEdgeの定義の中でNodeを登場させたい、ということがあったとしましょう。

コンパイラはファイルを先頭から最後まで順に処理するので、NodeとEdgeのどちらを定義するのか、決めないといけません。Nodeを先に書いたとしましょう。

そうしたら、Nodeの中でEdgeを参照したいときには、Edgeの定義をまだ読み込んでいません。

このような場合に使える技として、「定義」ではなく「宣言」を先に済ませる、というのがあります。

次のように書きます。

class Node; // Nodeの宣言。定義は後ほど登場する。
            // 宣言により、"Node"がコンパイラに型名として認知される。

class Edge { // Edge の定義。Edgeの内容を記述する。
public:
   // Node の宣言しか、コンパイラは見ていない。
   // この状況でも可能な操作は、Nodeのポインタ型の変数の定義。
   // どの型のポインタも同じサイズなので、何かのポインタ、という情報だけで、
   // コンパイラは記憶領域のサイズを決定したり、コピー処理の命令を作り出せる
   // それ以上のことは、何もできない。
   Node *getSourceNode();
   Node *getDestinationNode();
   void setSourceNode(Node *n);
   void setDestinationNode(Node *n);
private:
   Node *source_node_;
   Node *destination_node_;
   // ファイルを下まで読むと Node には inward_edge_count_ なるメンバが
   // 定義されていることがわかるが、この行を処理している時点ではコンパイラは
  // それを見ていないので、たとえば source_node_->inward_edge_count_ = 0;
   // などと、ここに書くと、コンパイルエラーになる。
};

// あとから、Nodeを定義する

class Node {
public:
  int getOutwardEdgeCount();
  Edge *getOutwardEdge(int edge_index);
  void addOutwardEdge(Edge *e);
  int getInwardEdgeCount();
  Edge *getInwardEdge(int edge_index);
  void addInwardEdge(Edge *e);
private:
  // このNodeから出ていくedge
  int outward_edge_count_;
  Edge *outward_edges_[10]; // 最大10個まで記憶できる
  // このNodeに入ってくるedge
  int inward_edge_count_;
  Edge *inward_edges_[10];
};

このような書き方をすれば、相互に参照しあう型を定義できます。
上記の2つのクラス NodeとEdgeは、互いに密接に関係していて、つねにセットで
使われるでしょうから、典型的には1つのヘッダファイルに、2つのクラスを書きます。

NodeとEdgeを別々のヘッダファイルに分けて書くことも可能です。その場合には次のようにします。

  • Edge.h の中には Nodeの宣言とEdgeの定義を入れます。
  • Node.h の中には Edge.hのinclude指令と、Nodeの定義を入れます。

ファイル間の参照関係はあくまでも一方通行であり、どちらにどちらを参照させるのかは、設計者がしっかり決める必要があります。

C/C++はそういう言語です。

これがたとえばjavaだと相互に参照してもOKですね。コンパイラがファイルを読む読み方が、C/C++とはだいぶ違うので。

Correct answer by hideo.at.yokohama on October 17, 2020

test3.cpp#includeの内容を忠実に追えばわかることです。

#include<iostream>   // これは質問と関係ないので追わない
#include"test1.cpp"
  #include"test1.h"
    #ifndef test1_H_
    #define test1_H_       // test1_H_がここで定義される
    #include<iostream>     // これは質問と関係ないので追わない
    #include"test2.h"
      #ifndef test2_H_
      #define test2_H_
      #include<iostream>   // これは質問と関係ないので追わない
      #include"test1.h"
        #ifndef test1_H_   // test1_H_は定義済みなので#endifまで無視される
        #endif
      namespace B{
       A::K t;             // ここより上にAに関する宣言が存在しない
       void Func();
      };
      #endif
    namespace A{
     class K{
      public:
      int k;
      void main();
     };
    };
    #endif
  void A::K::main(){
    std::cout << A::K::k << std::endl;
  }
#include"test1.h"
#include"test2.cpp"
#include"test2.h"
int main(){
 B::Func();
}

見て明らかなように、 A::K t; に到達した時点で、 A に関する宣言・定義いずれもありません。ですのでエラーになります。


というように、#includeはいい感じに解決してくれる魔法のキーワードというわけではなく、愚直に指定された行に指定されたファイルの内容をインクルードする機能です。
逆に言えば、#include結果は開発者にも正確に理解できる機能であり、test3.cppで書かれた

#include"test1.cpp"
#include"test1.h"
#include"test2.cpp"
#include"test2.h"`

のように何でもかんでも#includeすれば問題が解決できるなどと考えるべきではありません。もっとご自身の書いたソースコードに目を向けましょう。

Answered by sayuri on October 17, 2020

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP