組み込みアプリケーションでIARコンパイラを使用する必要があります(名前空間、例外、多重/仮想継承がなく、テンプレートは少し制限されており、C++ 03のみがサポートされています)。パラメーターパックを使用できないため、可変パラメーターを使用してメンバー関数を作成しようとしました。可変パラメータは一般的に安全ではないことを知っています。しかし、_va_start
_マクロでthis
ポインタを使用しても安全ですか?
通常の可変部分関数を使用する場合、残りのパラメーターにアクセスできるようにするには、_...
_の前にダミーパラメーターが必要です。可変長マクロが_...
_の前にパラメーターを必要としないことはわかっていますが、使用しないほうがよいでしょう。メンバー関数を使用する場合は、_...
_の前にthis
パラメーターが非表示になっているので、試してみました。
_struct VariadicTestBase{
virtual void DO(...)=0;
};
struct VariadicTest: public VariadicTestBase{
virtual void DO(...){
va_list args;
va_start(args, this);
vprintf("%d%d%d\n",args);
va_end(args);
}
};
//Now I can do
VariadicTestBase *tst = new VariadicTest;
tst->DO(1,2,3);
_
tst->DO(1,2,3);
は、期待どおりに123を出力します。しかし、それが単なるランダム/未定義の動作ではないのかどうかはわかりません。 tst->DO(1,2);
は通常のprinfと同じようにクラッシュします。私は気にしない。
標準でその動作を指定するものは何もないため、この構成体は正式な未定義の動作を呼び出すだけです。つまり、実装で正常に機能し、別の実装でコンパイルエラーや予期しない結果を引き起こす可能性があります。
非静的メソッドが隠しthis
ポインターを渡す必要があるという事実は、va_start
使用できます。初期の頃、C++コンパイラはC++ソースをCソースに変換するプリプロセッサであり、非表示のthis
パラメータがプリプロセッサによってCコンパイラで使用できるように追加されていたため、おそらくこのように機能します。そして、おそらくcompatibilityの理由で維持されています。しかし、私はミッションクリティカルなコードでそれを避けるために一生懸命努力します...
未定義の動作のようです。多くの実装でva_start(ap, pN)
が何をするか(ヘッダーファイルを確認)を見ると、pNのアドレスが取得され、pNのサイズだけポインターがインクリメントされ、結果がapに格納されます。合法的に&this
?
私はここに素敵な参照を見つけました: https://stackoverflow.com/a/9115110/10316011
2003 C++標準を引用すると:
5.1 [expr.prim] thisキーワードは、非静的メンバー関数(9.3.2)が呼び出されるオブジェクトへのポインターを指定します。 ...式のタイプは関数のクラス(9.3.2)へのポインタです、...式は右辺値です。
5.3.1 [expr.unary.op]単項&演算子の結果は、そのオペランドへのポインターです。オペランドは左辺値またはqualified_idでなければなりません。
したがって、これで問題が解決したとしても、保証されているわけではなく、信頼してはいけません。
私はそれは大丈夫だと思いますが、そう言っているC++標準からの特定の引用が見つかるかもしれません。
理論的根拠はこれです:va_start()
には、関数の最後の引数を渡す必要があります。明示的なパラメーターをとらないメンバー関数にはパラメーターが1つ(this
)しかないため、これは最後のパラメーターでなければなりません。
ユニットテストを追加して、これが機能しないプラットフォームでコンパイルした場合に警告を出すのは簡単です(これはありそうもないことですが、再び、やや非定型のプラットフォームでコンパイルしています)。
これは未定義の動作です。言語はthis
をパラメーターとして渡す必要がないため、まったく渡されない場合があります。
たとえば、コンパイラがオブジェクトがシングルトンであることを理解できる場合、this
をパラメーターとして渡すことを回避し、this
のアドレスが明示的に必要な場合にグローバルシンボルを使用できます( va_startの場合)。理論的には、コンパイラーはva_start
でそれを補償するコードを生成する可能性があります(結局のところ、コンパイラーはこれがシングルトンであることを認識しています)が、標準ではそうする必要はありません。
次のようなものを考えてください:
class single {
public:
single(const single& )= delete;
single &operator=(const single& )= delete;
static single & get() {
// this is the only place that can construct the object.
// this address is know not later than load time:
static single x;
return x;
}
void print(...) {
va_list args;
va_start (args, this);
vprintf ("%d\n", args);
va_end (args);
}
private:
single() = default;
};
Clang 8.0.0などの一部のコンパイラは、上記のコードに対して警告を発します。
prog.cc:15:23: warning: second argument to 'va_start' is not the last named parameter [-Wvarargs] va_start (args, this);
警告にもかかわらず、 それは正常に実行されます 。一般的に、これは何も証明しませんが、警告を出すのは悪い考えです。
注:シングルトンを検出して特別に処理するコンパイラは知りませんが、言語はこの種の最適化を禁止していません。コンパイラーによって今日行われていない場合、明日は別のコンパイラーによって行われる可能性があります。
注2:それにもかかわらず、これをva_start
に渡すと実際に機能する場合があります。動作しても、規格で保証されていないことを行うことはお勧めできません。
注3:同じシングルトン最適化は、次のようなパラメーターには適用できません。
void foo(singleton * x, ...)
シングルトンを指すか、nullptr
の2つの値のいずれかを持っている可能性があるため、最適化することはできません。つまり、この最適化の問題はここでは適用されません。