面接したばかりです。「前方宣言」とは何かと聞かれました。次に、それらが前方宣言に関連する危険であるかどうかを尋ねられました。
2番目の質問には答えられませんでした。ネットで検索しても、興味深い結果は表示されませんでした。
それで、誰かが前方宣言の使用に関連する危険を知っていますか?
面接したばかりです。「前方宣言」とは何かと聞かれました。次に、それらが前方宣言に関連する危険であるかどうかを尋ねられました。
2番目の質問には答えられませんでした。ネットで検索しても、興味深い結果は表示されませんでした。
それで、誰かが前方宣言の使用に関連する危険を知っていますか?
まあ、複製に関する問題は別として...
...標準には少なくとも1つの痛い箇所があります。
不完全な型へのポインタを呼び出すdelete
と、未定義の動作が発生します。実際には、デストラクタは呼び出されない場合があります。
次のコマンドとサンプルを使用して、 LiveWorkSpaceでそれを確認できます。
// -std=c++11 -Wall -W -pedantic -O2
#include <iostream>
struct ForwardDeclared;
void throw_away(ForwardDeclared* fd) {
delete fd;
}
struct ForwardDeclared {
~ForwardDeclared() {
std::cout << "Hello, World!\n";
}
};
int main() {
ForwardDeclared* fd = new ForwardDeclared();
throw_away(fd);
}
診断:
Compilation finished with warnings:
source.cpp: In function 'void throw_away(ForwardDeclared*)':
source.cpp:6:11: warning: possible problem detected in invocation of delete operator: [enabled by default]
source.cpp:5:6: warning: 'fd' has incomplete type [enabled by default]
source.cpp:3:8: warning: forward declaration of 'struct ForwardDeclared' [enabled by default]
source.cpp:6:11: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined
警告してくれたコンパイラに感謝しませんか;)?
どんな危険も利益によって隠されていると思います。ただし、主にリファクタリングに関連するものもあります。
namespace
ディレクティブと組み合わせてクラスを別のクラスに移動するとusing
、大混乱を引き起こす可能性があります(不思議なエラー、見つけて修正するのが難しい)-もちろん、using
ディレクティブは最初から悪いですが、完璧なコードはありませんよね?*検討
template<class X = int> class Y;
int main()
{
Y<> * y;
}
//actual definition of the template
class Z
{
};
template<class X = Z> //vers 1.1, changed the default from int to Z
class Y
{};
その後、クラスZ
はデフォルトのテンプレート引数として変更されましたが、元の前方宣言は引き続きint
。
意味:
//3rd party code
namespace A
{
struct X {};
}
および前方宣言:
//my code
namespace A { struct X; }
//3rd party code
namespace B
{
struct X {};
}
namespace A
{
using ::B::X;
}
これは明らかに私のコードを無効にしましたが、エラーは実際の場所ではなく、修正は控えめに言っても怪しいものでした。
前方宣言は、 C ++にモジュールがない(C ++ 17で修正される予定ですか?)ことと、ヘッダーを含めることの症状です。C++にモジュールがある場合、前方宣言はまったく必要ありません。
前方宣言は「コントラクト」以上のものです。これを使用することで、実際に何かの実装を提供することを約束します(同じソースファイルの後で、または後でバイナリをリンクすることによって)。
その短所は、実際には契約に従わなければならないということです。契約に従わない場合、コンパイラはどういうわけか早期に文句を言うので、それほど問題にはなりませんが、一部の言語では、コードは「自分の存在」(動的に型付けされた言語と言えば)
不完全なクラス型へのポインタがに渡されるdelete
と、operator delete
オーバーロードが見落とされる可能性があります。
私が得たのはそれだけです…そして噛まれるには、ソースファイルで「不完全な型」のコンパイラエラーを引き起こすようなことは何もしなくてはなりません。
編集:他の人の先導に従って、私は、前方宣言が実際に実際の宣言と一致することを保証することは困難(危険と見なされる可能性があります)だと思います。関数とテンプレートの場合、引数リストの同期を維持する必要があります。
そして、それが宣言するものを削除するとき、あなたは前方宣言を削除する必要があります、さもなければ、それは周りに座って名前空間を作り上げます。しかし、そのような場合でも、コンパイラは、邪魔になるとエラーメッセージでそれを指し示します。
より大きな危険は、前方宣言がないことです。ネストされたクラスの主な欠点は、前方宣言できないことです(まあ、それらは囲んでいるクラススコープ内にある可能性がありますが、それは簡単です)。
何かを前方宣言することの唯一の危険は、ヘッダーの外側または非共有ヘッダーで前方宣言を行う場合であり、前方宣言の署名は、前方宣言されているものの実際の署名とは異なります。で行うと、extern "C"
リンク時に署名をチェックするための名前マングリングが発生しないため、署名が一致しない場合に未定義の動作が発生する可能性があります。
GoogleC ++スタイルガイドで興味深いスニペットに出くわしました
彼らが指摘する危険は、不完全な型に関数を実装することから生じます。通常、これはコンパイラーがエラーをスローしますが、これらはポインターであるため、ネットをすり抜けることがあります。
前方宣言または完全な#includeが必要かどうかを判断するのは難しい場合があります。#includeを前方宣言に置き換えると、コードの意味を黙って変更できます。
// b.h: struct B {}; struct D : B {}; // good_user.cc: #include "b.h" void f(B*); void f(void*); void test(D* x) { f(x); } // calls f(B*)
#includeがBおよびDの転送declに置き換えられた場合、test()はf(void *)を呼び出します。
前方宣言のもう1つの危険性は、単一定義規則に違反しやすくなることです。ah前方宣言class B
(bhとb.cppにあるはずです)があると仮定しますが、a.cpp内に、bhとは異なる宣言をするb2.hを実際に含めるとclass B
、未定義の動作になります。
前方宣言自体はそれほど危険ではありませんが、コードの臭いです。前方宣言が必要な場合は、2つのクラスが緊密に結合されていることを意味しますが、これは通常は不適切です。したがって、コードのリファクタリングが必要になる可能性があることを示しています。
密結合があっても問題ない場合があります。たとえば、状態パターン実装の具象状態が密結合している場合があります。これで大丈夫だと思います。しかし、他のほとんどの場合、前方宣言を使用する前に設計を改善します。
最初の方法は、関数呼び出しを並べ替えて、mainの前にaddが定義されるようにすることです。
そうすれば、main()がadd()を呼び出すまでに、addが何であるかがすでにわかります。これは非常に単純なプログラムであるため、この変更は比較的簡単に実行できます。ただし、大規模なプログラムでは、どの関数が他のどの関数を呼び出したかを解読して、正しい順序で宣言できるようにするのは非常に面倒です。