item 8:0やNULLよりnullptrが好き

5288 ワード

本文はmodern effective C++から翻訳して、レベルが限られているため、翻訳が完全に正しいことを保証することができなくて、間違いを指摘することを歓迎します.ありがとう!
まずいくつかの概念を見てみましょう.字面の0はintで、ポインタではありません.C++が0がコンテキストでポインタとしてしか使用できないことを発見した場合,それは無理に0をnullポインタとして解釈するが,これは歪のスキームにすぎない.C++の主なルールはやはり0をintと見なし,ポインタではない.
実際、NULLもそうです.NULLの場合、C++規格は、intが数値タイプ(例えばlong)を1つ与える必要があるかどうかにかかわらず、実装を可能にするため、詳細にはいくつかの不確実性がある.これは0とは異なるが、ここでの問題はNULLのボディタイプではなく、0とNULLがポインタタイプではないため、実際には関係ない.
C++98では,この問題がもたらす最も主要な影響はポインタと数値タイプのリロードが予期せぬ事態をもたらすことである.このようなリロードに0またはNULLを入力すると、ポインタバージョンのリロードは呼び出されません.
void f(int);        //f     
void f(bool);
void f(void*);

f(0);               //  f(int),    f(void*)

f(NULL);            //        ,       f(int),
                    //      f(void*)

f(NULL)挙動に関する不確実性は、NULLタイプの実装(コンパイラ)が自由な権利を有することを反映している.例えば、NULLが0 Lと定義されている場合(すなわちlongタイプの0)では、関数の呼び出しに曖昧さが生じる.longをint,bool,void*に変換することは同様に可能であるからである.fを呼び出す興味深いことは、ソースコードの表面上の意味(NULL---nullポインタを用いてfを呼び出す)とその実際の意味である(私はある種の整形を使う---ポインタではなく、fを呼び出す)間の矛盾.このような反直感的な行為はC++98プログラマーの指導方針を招き、ポインタタイプと整形タイプをリロードすることを避ける.この方針はC++11において依然として有効である.なぜなら、このItemの提案があってもnullptrはより良い選択であるにもかかわらず、一部の開発者は0とNULLを引き続き使用するからである.
nullptrの利点は、整形タイプではないだけではありません.正直に言うと、ポインタタイプではありませんが、すべてのタイプを指すポインタだと考えることができます.nullptrの実際のタイプはstd::nullptr_t、完璧なループ定義(circular definition)では、std::nullptr_tはnullptrのタイプとして定義されます.std::nullptr_tタイプは、すべての元のポインタタイプに暗黙的に変換できるため、nullptrはすべてのタイプのポインタのように表現されます.
nullptrでfのリロード関数を呼び出すと、nullptrは数値タイプとみなされないため、実際にはvoid*バージョンの関数(つまりポインタバージョンのリロード)が呼び出されます.
f(nullptr);         //  f(void*)       

従ってnullptrを用いて0とNULLの代わりに意外な再負荷解析を回避したが,これは彼の唯一の利点ではない.特にauto変数に関連する場合、コードの可読性も向上します.たとえば、次のコードに遭遇したとします.
auto result = findRecord(/* arguments*/);

if(result == 0){
    ...
}

たまたまfindRecordが返すタイプを知らない(または見つけにくい)場合は、resultがポインタタイプなのか整形タイプなのかは不明です.つまり、0(resultと比較するために使用される)は2つのうちのいずれかです.ただし、次のコードを見てください.
auto result = findRecord(/* arguments*/);

if(result == nullptr){
    ...
}

ここでは曖昧さはありません.resultはポインタタイプに違いありません.
templateが関与するとnullptrはより輝きます.適切な反発量がロックされてから呼び出すことができる関数があるとします.各関数には、異なるタイプのポインタが使用されます.
int     f1(std::shared_ptr<Widget> spw);
double  f2(std::unique_ptr<Widget> upw);
bool    f3(Widget* pw);

nullポインタを入力して関数を呼び出すコードは、次のように見えます.
std::mutex f1m, f2m, f3m;

using MuxGuard =                //C++11  typedef, Item 9
    std::lock_guard<std::mutex>;
...

{
    MuGuard g(f1m);             //  f1    
    auto result = f1(0);        //  0  null   f1
}                               //     

...

{
    MuGuard g(f2m);             //  f2    
    auto result = f2(NULL);     //  NULL  null   f2
}                               //     

...

{
    MuGuard g(f3m);             //  f3    
    auto result = f3(nullptr);  //  nullptr  null   f3
}                               //      

nullptrで前の2つの関数を呼び出すことができないのは不幸ですが、コードが正常に動作することが重要です.しかし、コード内の重複モード(ロック、呼び出し関数、ロック解除)は不幸であるだけでなく、さらに不安である.このソースコードの重複問題はtemplateが設計されて解決された1つであるため、このモードをテンプレート化しましょう.
template<typename FuncType,
         typename MuxType,
         typename PtrType>
auto lockAndCall(FuncType func,
                 MuxType& mutex,
                 PtrType ptr) -> decltype(func(ptr))
{
    MuxGuard g(mutex);
    return func(ptr);
}

もしこの関数の戻りタイプ(auto...->decltype(func(ptr))があなたの脳を爆撃したら、あなたの脳はItem 3(何が起こったのか説明した)を経験して賛成したことがありますか?C++14バージョンのコードも見えます.戻りタイプは簡単なdecltype(auto):
template<typename FuncType,
         typename MuxType,
         typename PtrType>
decltype(auto) lockAndCall(FuncType func,   //C++14
                           MuxType& mutex,
                           PtrType ptr) 
{
    MuxGuard g(mutex);
    return func(ptr);
}

lockAndCalltemplate(各バージョン)の呼び出し関数を与えます.
auto result1 = lockAndCall(f1, f1m, 0);         //  

...

auto result2 = lockAndCall(f2, f2m, NULL);      //  

...

auto result3 = lockAndCall(f3, f3m, nullptr);   //  

はい、そう書くことができますが、注釈のように、前の2つの場合、コードはコンパイルできません.1つ目の呼び出しの問題は、0がlockAndCallに入力されると、templateタイプの導出がそのタイプを見つけることです.0のタイプは過去は,現在は,未来も永遠にintであるため,これがインスタンス化されたlockAndCallにおけるptrパラメータのタイプである.残念なことに、lockAndCallではfuncが呼び出されるとintタイプが入力されるが、f 1に必要なstd::shared_patrパラメータは互換性がありません.lockAndCall呼び出しではnullポインタの代わりに入力0を試みたが,実際には通常のintが入力された.std::shared_としてintを入力しようとします.ptrがf 1に与えるとタイプエラーが発生します.0でlockAndCallを呼び出すと失敗します.なぜなら、templateでintがstd::shared_を必要とするためです.ptrの関数.(注:理解は簡単ですが、ここではintからポインタタイプへの変換はなく、0のみがポインタとみなされます.したがって呼び出しは失敗します.)
呼び出しに関するNULLの解析は本質的に0と同じである.NULLがlockAndCallに入力されると、整形タイプがptrパラメータのタイプとして導出され、intまたはクラスintタイプのパラメータがf 2に入力される(std::unique_ptrタイプのパラメータが必要)と、タイプエラーが発生する.
対照的にnullptrに関連する呼び出しは問題ありません.nullptrがlockAndCallに渡されると、ptrのタイプはstd::nullptr_と導出される.t.ptrがf 3に転送されると、ここでstd::nullptr_std::nullptr_tは任意のタイプのポインタに暗黙的に変換することができる.
実際、nullポインタを使用したい場合は、0とNULLに対して、templateタイプは、0またはNULLの代わりにnullptrを使用する最も重要な理由である「エラー」のタイプ(すなわち、それらの「ペア」の「タイプはnullポインタを表すはずだった)を導出します.nullptrを使用すると、templateは特別な課題に直面しません.nullptrと組み合わせると、予期せぬリロード解析を受けることはありません.(0とNULLが受ける可能性が高い)事実は明らかです.nullポインタを使用したい場合は、0やNULLではなくnullptrを使用します.
君が覚えていること
  • 0とNULLよりnullptr
  • を好む
  • リロード整形タイプとポインタタイプを回避します.