web-dev-qa-db-ja.com

C ++ / Qt関数に常にすべての引数のnull値をチェックさせるのは良い習慣ですか?

バックストーリー

Qt Signal/Slotsで開発しているときに、何が原因であるのか戸惑ういくつかのセグメンテーション違反に遭遇しました。最終的に私は、引数を提供せずに実際にスロット関数を渡すことができることを理解しました。もちろん、引数が使用された場合、セグメンテーション違反が発生します。

この落とし穴を回避するために、すべての引数をチェックして、それらがNULLでないことを確認する習慣をつけています。

void MyClass::setMyString(QString input)
{
    if (input.isNull()) {
        qDebug() << "Error: setMyString() received NULL argument";
        return;
    }

    m_MyString = input;
    emit myStringChanged();
}

これからallを使ってこれを行うべきだと思います。推論の一部は、これを行うことは害がないことを私が理解していること、そして私または他の誰かが私のコードをリファクタリングする必要がある場合、彼らが必要なnull値チェックを省略しそうにないことです。

質問:

  1. これはallまたはmostの状況で受け入れ可能なスタイルですか?
  2. 単に関数を返すのではなく、実行時エラーを導入してプログラムを停止する必要がありますか?
  3. このスタイルのコーディングはセキュリティを向上させますか?
  4. このスタイルのコーディングは安定性を向上させますか?
  5. タイプに応じてnull値のチェックに時間がかかる程度まで、パフォーマンスに関する考慮事項はありますか?
  6. C++とQtのほかに、このスタイルから他のどの言語にメリットがありますか?
  7. 検討する価値のある重大な欠点はありますか?
5
Akiva

ここでバジルをエコーするようなものですが、少しネガティブなトーンを持っています。私も「はい」と言いますが、前提条件のsilent違反を許可する場合は注意してください。個人的な例として、私はAPIを介してこのようなことを行うCコードベースで作業しました。

_int f(struct Foo* foo)
{
    if (!foo)
        return error;
    ...
    return success;
}
_

おそらく、途中にデバッグログがあってもです。しかし、それは大規模なコードベース、数千万行のコード、30年以上の開発、そしてかなりお粗末な標準でした。ログのような場所での警告は、開発者が出力を無視する習慣を身につけたことでいっぱいになりました。

私は、最も残念なことに、そのようなエラーチェックを不愉快に大げさなアサーションに変更しようとすると、これらの前提条件に違反し、単にエラーを無視しているシステムに何千もの場所があったことがわかりました(報告された場合、それらのケースでは何でも)。開発者はそれを回避し、コードが「機能する」まで試行錯誤し、コードを追加し続けました。それを許可しない関数にnullを渡していたという元の間違いに気付かなかったのです。

そのため、次のようにすることを強くお勧めします。

_void MyClass::setMyString(QString input)
{
    assert(!input.isNull() && "Error: setMyString() received NULL argument.");
    m_MyString = input;
    emit myStringChanged();
}
_

これにより、アサーションが失敗した場合にアプリケーションが重大な停止状態になり、アサーションが失敗した正確なソースファイルと行番号がこのメッセージとともに表示されます。

これは、関数がnullを受け取ることが許可されていない場合に使用します(この場合、「null」は「空の文字列」を意味します)。許可されているものについては、単に何か他のことをするかもしれませんが、それはバグを報告しません(そのためには、無視できない大声で不愉快なassertを使用してください)。

ifの使用例は次のようになります。

_bool do_something(int* out_count = 0)
{
    ...
    if (out_count)
       *out_count = ...; // write the result back.
    return true;
}
_

Nullを受け取ることが許可されていないものについては、可能な限り参照を使用してください。ただし、スマートポインターを受け入れる場合や、別の例として「null」の定義を拡張して空の文字列を含める場合でも、「null」が発生する可能性があります。そのような場合、可能であれば、前提条件を自由にassertにしてください。 assertには、アプリケーションを完全に停止するだけでなく(良いこと)、デバッグセッションの外でも(ただし、デバッグビルドを使用しても)中断した場所を指摘できるという利点があります。例)そして、それはまた、文書化の効果的な手段です。

qDebugは通常、少し静かです。 neverがそれらの出力を無視することを許可する良い習慣のあるチームは、それらをゼロに保つことで回避できるかもしれません。しかし、assertは、プロセス全体を非常に停止して、これらのプログラマエラーを無視することを不可能にすることで、最も不作法なチームからも保護します。 assertのもう1つの小さな利点は、リリース(本番)ビルドの速度を低下させることができないのに対し、if (...) qDebug(...)の速度は低下することです。

コントロール外の外部ソースによって引き起こされたエラーの場合(例:ダウンしていたサーバーへの接続の失敗、大量のメモリブロックの割り当ての失敗、ユーザーがロードしようとしたファイルからの読み取りの試行、破損など)例外をスローしますが、境界外の配列にアクセスしたり、nullを受け取ってはならない場所でnullを受け取ったりするようなプログラマーのバグは例外です(ソフトウェアがプログラマーからでも優雅に回復しようとする非常にミッションクリティカルなソフトウェアで作業している場合を除く)。バグをできるだけ簡単に検出および再現できるようにするのではなく)。これらの場合、assert

10
user204677

私がコメントしたように、はい、通常、パブリック関数がnullポインターを受け入れるかどうかをnullptrおよびalways documentに対してチェックする必要があります(つまり、関数がnullポインターを受け入れるものとして文書化するか、 null以外の有効なポインタが必要な場合)。

nullptrに対するチェックは-ほとんどの現在のC++ 11実装では-非常に迅速です。ポインターを逆参照するとキャッシュミスが発生する可能性があります(常に遅い)が、それを0と比較すると高速な算術演算(ほぼ無料) 。

ポインターが常にnullで有効でなければならない場合は、assertを使用します。

実行時に本当に可能であれば、ifを使用してnullptrのケースを処理します。

C++参照を使用して、アドレスが常に有効であることを伝えます。つまり、void foo(int*)があり、アドレスがnullではなく常に有効であることがわかっている場合は、代わりにvoid foo(int&)関数をコーディングします。

コンパイラーは、nullポインターテストの最適化を許可されていますafter逆参照しました:

 void foo(struct bar_st*p) {
   int x = p->somefield;
   if (!p) return; /// test can be removed by optimization

引数を指定せずに実際にスロット関数を渡すことができることを理解しました。もちろん、引数が使用された場合、セグメンテーション違反が発生します。

それは一体何の意味ですか?それはgoobledygookです。一般的に言えば、内部的にバイナリ互換性のないプロジェクトを構築できなかった場合(qmakeは常にそれから保護されない!)、Qtは未定義の引数でスロットを呼び出すことができません。スロットにデフォルトの引数値がある場合、それらの引数を指定せずに呼び出すと、デフォルト値が置き換えられます。ただし、スロット呼び出しに引数があり、その値が少なくともで値初期化されたデフォルト値に設定されないという状況は決してありません。つまりQtはバイナリ互換性のない方法でメソッド/関数を盲目的に呼び出しません。あれは:

_#include <QtCore>
class Class : public QObject {
   Q_OBJECT
public:
   Q_SIGNAL void mySignal1();
   Q_SIGNAL void mySignal2(const QString & = {});
   Q_SIGNAL void mySignal3(const QString &);
   Q_SIGNAL void mySignal4(QObject *);
   Q_SLOT void myMethod1(const QString & = {}) {}
   Q_SLOT void myMethod2(const QString &) {}
   Q_SLOT void myMethod3(QObject *) {}
};

int main() {
   QLoggingCategory::setFilterRules("default.warning=false");
   Class c;
   bool r;
   r = QMetaObject::invokeMethod(&c, "myMethod1");
   Q_ASSERT(r); // calls myMethod1, all parameters have values
   r = QMetaObject::invokeMethod(&c, "myMethod2");
   Q_ASSERT(!r); // doesn't call, value needed for first parameter
   r = QMetaObject::invokeMethod(&c, "myMethod2", Q_ARG(QString, "foo"));
   Q_ASSERT(r); // calls myMethod2, value is given for first parameter
   r = QMetaObject::invokeMethod(&c, "myMethod3");
   Q_ASSERT(!r); // doesn't call, value needed for first parameter
   r = QObject::connect(&c, SIGNAL(mySignal1()), &c, SLOT(myMethod1()));
   Q_ASSERT(r); // succeeds, myMethod1 has a default-value signature
   c.mySignal1(); // perfectly defined behavior: mySignal1 gets a default-constructed argument
   r = QObject::connect(&c, SIGNAL(mySignal1()), &c, SLOT(myMethod2()));
   Q_ASSERT(!r); // fails - no such slot
   r = QObject::connect(&c, SIGNAL(mySignal2()), &c, SLOT(myMethod2(QString)));
   Q_ASSERT(!r); // fails - no such signal
   r = QObject::connect(&c, SIGNAL(mySignal2(QString)), &c, SLOT(myMethod2(QString)));
   Q_ASSERT(r);
   c.mySignal2(); // mySignal2 has a default value for the argument
   r = QObject::connect(&c, SIGNAL(mySignal3(QString)), &c, SLOT(myMethod1(QString)));
   Q_ASSERT(r);
   c.mySignal3({});
   r = QObject::connect(&c, SIGNAL(mySignal4(QObject*)), &c, SLOT(myMethod3(QObject*)));
   Q_ASSERT(r);
   c.mySignal4(nullptr); // explicit call with a null object, myMethod3 is presumed to accept such
}
#include "main.moc"
_

いずれの場合でも、文字列の長さをチェックしないなど、まったくばかげたことをしていない限り、null Qtコンテナ(つまりnull文字列)を使用しても動作は未定義ではありません(つまり、セグメンテーション違反にはなりません)。長さが役割を果たすときに取り組みます。そして、長さが重要なコンテキストで文字列を使用しようとしている場合にのみ役割を果たします。文字列の範囲内のインデックスを必要とする方法でコンテンツのインデックスを作成する場合。多くのメソッドは関係ありません。つまり、_QString::mid_は無効な位置を気にしません。代わりにnull文字列が返されます。

私はすべての引数をチェックして、それらがNULLでないことを確認する習慣をつけています。

あなたの例は、null pointerに対して何かをチェックすることとは何の関係もありません。これがC NULLです!

あなたの例のテストは、どういうわけか_m_myString_が空でなければならないのでない限り、かなり無意味です。そうだとは思いません。典型的なQtセッターは次のようになります。

_void MyClass::setMyString(const QString &input) {
  if (m_MyString != input) {
    m_MyString = input;
    // do whatever is needed when the string has changed, e.g.
    // call update() if MyClass is a widget.
    emit myStringChanged();
  }
}
_

Qtではonlyデフォルトで作成された文字列はnullであることに注意してください。したがって、null文字列のテストは非常に具体的です。これは、空の文字列のテストよりも強力なテストであり、他の動作に関しては、違いはありません。null文字列と空の文字列は同じように動作します。 isNull()からの戻り値以外。

経験則として、文字列のnull性は、outputの判別式としてのみ重要です。 _std::optional_がないため、これは貧乏人の回避策です。 null文字列値は、値の欠如を空の文字列と区別する必要がある特殊な場合に返される/返すことができます。入力に関する限り、Qtで私が知っているすべてのものは、入力のnull文字列を空の文字列として扱いますが、null文字列(またはその他のnull値-つまり、nullポインターまたはisNull() == true付きの値) )はnullバリアントに伝播します。ただし、null文字列は無効なバリアントにはなりません。次に例を示します。

_QVariant v;
v = "b";
Q_ASSERT(v.toString() == "b");
v = "";
Q_ASSERT(!v.isNull());
Q_ASSERT(!v.toString().isNull());
Q_ASSERT(v.toString().isEmpty());
QString b;
Q_ASSERT(b.isNull());
v = b;
Q_ASSERT(v.isNull());
Q_ASSERT(v.isValid()); // null but valid
Q_ASSERT(v.toString().isNull());
Q_ASSERT(v.toString().isEmpty());
Q_ASSERT(v.toString() == b);
Q_ASSERT(v != QVariant{}); // because it's valid

QObject o;
o.setProperty("foo", "a");
Q_ASSERT(o.property("foo") == "a");
o.setProperty("foo", QVariant{});
Q_ASSERT(!o.dynamicPropertyNames().contains("foo"));
o.setProperty("foo", QString{}); // not invalid anymore
Q_ASSERT(o.dynamicPropertyNames().contains("foo"));
Q_ASSERT(o.property("foo").isNull());
_

したがって、本当に何らかの理由でデフォルトで構築された(つまりnull)文字列の受信を望まない場合は、それをアサートする必要があります。デバッグ出力は不十分です。少なくともqWarningである必要があります。しかし、アサートが望ましいでしょう。したがって:

_void MyClass::setMyString(const QString &input) {
  Q_ASSERT(!input.isNull());
  if (input.isNull()) { // for when QT_NO_DEBUG - i.e. release code
    qWarning() << "Error: MyClass::setMyString() invoked with a null parameter";
    return;
  }
  if (m_MyString != input) {
    ...
  }
}
_

これはすべてまたはほとんどの状況で受け入れ可能なスタイルですか?

いいえ。問題がある場合は、単なるqDebugでは不十分です。リリースアサートでない場合は、デバッグビルドのアサーションと、少なくともリリースビルドの警告が必要です。理想的には、_ASSERT_ALWAYS_を実装して、そのような安価なチェックに代わりに使用します。

_#define ASSERT_ALWAYS(cond) ((cond) ? static_cast<void>(0) : qt_assert(#cond, __FILE__, __LINE__))
_

単に関数を返すのではなく、実行時エラーを導入してプログラムを停止する必要がありますか?

それはセマンティクスに依存します-つまり-MyStringの意味。空のときにMyStringがまったく意味をなさない場合は、isNullチェックではなくASSERT_ALWAYS(!input.isEmpty())を使用してプログラムを終了する必要があります。チェックは安価であり、チェックのオーバーヘッドは、シグナルを送信するコストに比べてそれほど高くありません。 MyClassが空のMyStringを許可する場合、nullチェックは不要です。 null文字列は空の文字列ですが、空の文字列は必ずしもnullである必要はありません。

このスタイルのコーディングはセキュリティを向上させますか?

あんまり。 _MyClass::MyString_を空にできない場合、それを空に設定しようとすると、何かがひどく間違っています。問題の良さを理解することはできません。そのため、_ASSERT_ALWAYS_が唯一の健全な方法です。問題をログに記録して、その場で死にます。

Erlangのようにすばやく失敗したい場合、アサートは何らかの_assert_exception_をスローし、例外セーフコードを記述し、問題が発生した場合は再起動しますが、そのようなスタイルには規律が必要であり、問題を、1つのスレッドで実行されているErlangプロセスと同等のものに分割します。

このスタイルのコーディングは安定性を向上させますか?

安定性を向上させるシナリオは2つだけあります。つまり、_ASSERT_ALWAYS_を実行する場合-最初の論理エラーで継続を防ぐ場合、または失敗する概念プロセスが特別なものではないフェイルファストErlangスタイルのコードを作成する場合、そして、あなたはそれを再起動する手段を持っています。中途半端な「引数が間違っている場合はチェックして何もしない」は、クラスのユーザーの明確な論理エラーを無視し、そのようなエラーは重要ではないと想定しているため、通常、悲惨な結果をもたらします。 しかし、クラスはこれらのエラーが本当に重要でないことを知りません。

タイプに応じてnull値のチェックに時間がかかる程度まで、パフォーマンスに関する考慮事項はありますか?

繰り返しますが、ここでQtコンテナーについて話しているので、他の場所のコードはすでに空のコンテナーに対応しているはずです。他の場所のコードで処理できない場合、つまりMyStringの長さが1以上のクラスの不変条件である場合、もちろん、チェックを追加するとパフォーマンスにわずかな影響があります。 all nullをサポートする(多くはない)Qtコンテナーのチェックのコストは、ポインター比較、または最大2つです。

C++とQtのほかに、このスタイルから他のどの言語にメリットがありますか?

実装の詳細であるnull値に集中しているため、これを答えることは不可能です。 null値は重要ではありません。重要なのは、使用されるコンテキストでのその値のセマンティクスです。 null値が関数の前提条件(この場合:setMyString)に違反している場合は、もちろんそれをアサートする必要があります。これは安価なチェックであるため、_ASSERT_ALWAYS_である必要があります。コーディングスタイルや環境がフェイルファストをサポートしている場合は、失敗し、ユーザーコードに失敗したプロセス全体を再起動させる必要があります(このプロセスは、含まれる作業単位を意味し、OSプロセスとは関係ありません)。

検討する価値のある重大な欠点はありますか?

はい。あなたがそれを間違った側から見ているという事実-null QStringはほとんど問題になりません。クラスのセマンティクス、およびそのコンテキストのnull値、クラスの不変条件、および関数の前提条件が重要であり、これらの用語で問題を確認する必要があります。セグメンテーション違反の原因として認識されているものは何でも、null QStringsと関係があると感じています。結局のところ、これらは空の文字列の特殊なケースであり、他の空の文字列と同じように操作できます。例えば。これは有効です:

_QString a; // null string is also an empty string
Q_ASSERT(a.isNull());
Q_ASSERT(a.isEmpty());
qDebug() << a;
Q_ASSERT(a.size() == 0);
a += QStringLiteral("b");
Q_ASSERT(a == QStringLiteral("b"));
_

そして完全に明確にするために:次はnull文字列ではありません。それはそもそも文字列ではないので、どんな形や形のnull文字列でもありません。

_QString *s = nullptr;
_

これは文字列へのnullポインタです。文字列ではありません。大きな違い。そして、質問には正確にzeroポインタから文字列へのポインタが含まれていました。

1