web-dev-qa-db-ja.com

NULLで初期化されたstd :: stringのコンパイル時チェック

これは、 std :: stringパラメータに渡される0からどのように最善の方法で保護するかを補完する質問のようなものです 。基本的に、コードパスがstd::stringchar*コンストラクターをNULLを使用して無条件に呼び出そうとする場合にコンパイラーに警告する方法があるかどうかを把握しようとしています。

実行時チェックはすべてうまく機能していますが、次のような場合に適しています。

std::string get_first(const std::string& foo) {
    if (foo.empty()) return 0; // Or NULL, or nullptr
    return foo.substr(0, 1);
}

そのコードパスが実行された場合にコードが失敗することが保証されているにもかかわらず、システムヘッダーには通常、ポインターがnullであってはならないという前提条件が付けられていますが、これはgccの下でコンパイルを渡しますclangなど、-std=c++11 -Wall -Wextra -pedantic -Werrorを使用した場合も同様です。 gcc0の特定のケースを-Werror=zero-as-null-pointer-constantでブロックできますが、これはNULL/nullptrには役立ちません。関連するが類似していない問題に取り組む。主な問題は、プログラマが0NULL、またはnullptrを使用してこの間違いを犯し、コードパスが実行されていない場合に気付かないということです。

コード全体でstd::stringを特別なサブクラスで置き換えるなどのナンセンスなしに、コードベース全体をカバーするこのチェックをコンパイル時間にすることは可能ですか?

7
ShadowRanger

すべてのコンパイラで機能するか、非常に制限された状況で一部の副作用があるか、または一部のみで機能するかによって、さまざまなアプローチがありますが、交換の範囲ははるかに広くなります。

  1. 使用時に文句を言うオーバーロードを追加します。 _[[deprecated]]_ があります。C++ 11は、通常のシステムヘッダーのように抑制されない限り、即時の呼び出しサイトで文句を言うからです。

    GCCとCLANGは、より適切なカスタム属性 __attribute__((error("message"))) を提供します。これは、関数が使用され、呼び出しサイトに名前を付けると、常にビルドを中断します。

    Nullpointer-literalsである可能性のあるすべてのものを受け入れるオーバーロードを追加することの問題は、他のテンプレートのSFINAEを混乱させ、コードを破壊し、残念ながらnullpointerになってしまう_char*_型の引数をキャッチできないことです。 :

    _// added overload, common lines:
    template<class X, class = typename
        std::enable_if<std::is_arithmetic<X>() && !std::is_pointer<X>()>
    // best on GCC / clang:
    string(X) __attribute__((error("nullpointer")));
    // best on others:
    [[deprecated("UB")]] string(X) /* no definition, link breaks */;
    _
  2. 望ましい代替策は、引数をnull以外としてマークし、コンパイラーのオプティマイザーにそれを理解させることです。あなたはそのような警告を求め、それに注意を払っていますよね?
    これはGCCとCLANGのオプションにすぎませんが、追加のオーバーロードの不利な点を回避し、コンパイラーが理解できるすべてのケースをキャッチします。

    _basic_string(const CharT* s,
                 size_type count,
                 const Allocator& alloc = Allocator()) __attribute__((nonnull));
    basic_string(const CharT* s,
                 const Allocator& alloc = Allocator()) __attribute__((nonnull));
    _

    一般的に、GCC/clangに、コンパイル時に ___builtin_constant_p_ を使用して式の値を決定できるかどうかを尋ねることができます。

3
Deduplicator

コンパイル時には行われませんが、これを(少なくともほとんどの実装)std::stringが現在よりも適切に処理する文字列型を定義するのはかなり簡単です。

基本は、NULLint型を持ち、nullptrnullptr_t型を持つという事実です。そのため、これらの型をオーバーロードして、間違った型の引数を渡したときに「高速かつ大音量」で失敗するのはかなり簡単です。

#include <iostream>
#include <fstream>
#include <algorithm>
#include <iterator>
#include <vector>
#include <numeric>
#include <string>

class test_string {
    std::vector<char> data;
public:

    test_string(nullptr_t) { throw std::runtime_error("Bad input data"); }
    test_string(int) { throw std::runtime_error("Bad input data"); }
    test_string(void *) { throw std::runtime_error("Bad input data"); }
    test_string(char const *s) { size_t length = strlen(s); data.resize(length); std::copy_n(s, length, &data[0]); }
    size_t length() const { return data.size(); }

    friend std::ostream &operator<<(std::ostream &os, test_string const &s) {
        std::copy(s.data.begin(), s.data.end(), std::ostream_iterator<char>(os));
        return os;
    }
};

int main() {

    // These will all immediately throw if uncommmented:
    //test_string x(NULL);
    //test_string y(nullptr);
    //test_string z(0);

    // But this still works fine:
    test_string a("This is a string");
    std::cout << a;
}

std::stringでこれを行うのは、少し難しいかもしれません。問題は、std::stringには既にそのコンストラクタのかなりの数のオーバーロードがあることです。 nullptr_tのオーバーロードは常に安全である必要があります(その型の唯一のものはnullptrであり、明らかにそれを望まない)。

charT const *の代わりにintを取得するオーバーロードがあると、さらに困難になる可能性があります。問題は、オーバーロードの一部が、(たとえば)ポインターの位置と長さを示すカウントに基づいてインテントをすでに区別していることです。 NULLで呼び出しをキャッチしていることを確認するためにかなり注意深く調べる必要がありますが、(たとえば)charsize_tを必要とする既存の(正当な)使用を妨害していません。私はそれが不可能だとは確信していませんが、多少の注意が必要です(そして、まだ抜ける可能性のあるいくつかのケースを残しておく必要があるかもしれません)。

ただし、非常に実用的なものにするためには、おそらくstd::stringを変更する必要があります。これを別のクラスとして使用すると、かなり深刻な非実用性が(少なくとも私の意見では)発生します。

2
Jerry Coffin