web-dev-qa-db-ja.com

int vs const int&

私は通常、戻り値または引数として定数参照を使用していることに気付きました。その理由は、コードで非参照を使用するのとほぼ同じように機能するからだと思います。しかし、間違いなくより多くのスペースが必要になり、関数宣言が長くなります。私はそのようなコードで大丈夫ですが、一部の人々はそれが悪いプログラミングスタイルだと思うと思います。

どう思いますか? const int& over intと書く価値はありますか?とにかくコンパイラによって最適化されていると思うので、コーディングに時間を浪費しているだけかもしれません。

56
Pijusn

C++では、パラメータを処理するときにTを単に言うスマートな方法のように、const T&を使用するアンチパターンと考えるものは非常に一般的です。ただし、値と参照(constであるかどうかに関係なく)は2つの完全に異なるものであり、値の代わりに常に盲目的に参照を使用すると、微妙なバグにつながる可能性があります。

その理由は、参照を処理する場合、値では存在しない2つの問題、lifetimeおよびaliasingを考慮する必要があるためです。

例として、このアンチパターンが適用される場所の1つは標準ライブラリ自体であり、std::vector<T>::Push_backは値としてではなくconst T&をパラメーターとして受け入れ、これはたとえば次のようなコードで噛み付きます:

std::vector<T> v;
...
if (v.size())
    v.Push_back(v[0]); // Add first element also as last element

std::vector::Push_backはconst参照を必要としますが、Push_backを行うには再割り当てが必要になる場合があり、再割り当て後に受信した参照が無効になることを意味するため、このコードはカチカチ音をたてます(lifetime = issue)そして、未定義の動作領域を入力します。

値の代わりにconst参照が使用される場合、エイリアスの問題も微妙な問題の原因になります。たとえば、この種のコードに噛まれました:

struct P2d
{ 
    double x, y;
    P2d(double x, double y) : x(x), y(y) {}
    P2d& operator+=(const P2d& p) { x+=p.x; y+=p.y; return *this; }
    P2d& operator-=(const P2d& p) { x-=p.x; y-=p.y; return *this; }
};

struct Rect
{
    P2d tl, br;
    Rect(const P2d& tl, const P2d& br) : tl(tl), bt(br) {}
    Rect& operator+=(const P2d& p) { tl+=p; br+=p; return *this; }
    Rect& operator-=(const P2d& p) { tl-=p; br-=p; return *this; }
};

コードは一見非常に安全に見えます。P2dは二次元の点、Rectは長方形であり、点の追加/減算は長方形の平行移動を意味します。

ただし、Originで長方形を元に戻すためにmyrect -= myrect.tl;と書くと、コードは機能しません。これは、同じ場合に同じインスタンスのメンバーを参照する参照を受け入れる変換演算子が定義されているためです。

これは、tl -= p;で左上を更新した後、(0, 0)になるはずですが、pも同時に(0, 0)になることを意味しますpは左上のメンバーへの単なる参照であり、右下隅の更新は(0, 0)によって変換されるため機能しません。したがって、基本的には何もしません。

単語constのためにconst参照が値のようなものであると思い込まないでください。そのWordは、参照オブジェクトを変更しようとした場合にコンパイルエラーを与えるためにのみ存在しますその参照を使用ですが、参照オブジェクトが定数であることを意味するものではありません。より具体的には、const refによって参照されるオブジェクトは変更される可能性があり(たとえばaliasingのため)、使用中に存在しなくなることさえあります(lifetime issue)。

const T&では、Word constは、referenced objectではなく、referenceのプロパティを表します。これは、それを使用してオブジェクトを変更します。おそらく読み取り専用constが参照を使用している間、オブジェクトが一定になるという考えをプッシュするという心理的な効果があるので、より良い名前だったでしょう。

もちろん、特に大きなクラスの場合、値をコピーする代わりに参照を使用することで、印象的な高速化を実現できます。しかし、参照を使用するときは、他のデータへの単なるポインタであるため、参照を使用するときは常にエイリアスとライフタイムの問題について考える必要があります。ただし、「ネイティブ」データ型(int、double、pointer)の場合、参照は実際には値よりも遅くなり、値の代わりにそれらを使用しても何も得られません。

また、const参照は、コンパイラーが偏執的であることが強制されるため、オプティマイザーの問題を常に意味し、未知のコードが実行されるたびに、参照されるすべてのオブジェクトが異なる値(参照に対してconstオプティマイザーにとって絶対に意味がないことを意味します; Wordはプログラマーを助けるためだけに存在する-私は個人的にそれがそんなに大きな助けになるかどうかはわかりませんが、それは別の話です)。

134
6502

Oliが言うように、Tとは対照的にconst T&を返すことは完全に異なるものであり、特定の状況で壊れる可能性があります(彼の例のように)。

引数としてプレーンTとは対照的にconst T&を使用することは、物事を壊す可能性は低いですが、それでもいくつかの重要な違いがあります。

  • const T&の代わりにTを使用するには、Tがコピー構築可能であることが必要です。
  • Tを取得すると、コピーコンストラクターが呼び出されますが、コピーコンストラクターは高価になる可能性があります(また、関数終了時のデストラクタも)。
  • Tを使用すると、パラメーターをローカル変数として変更できます(手動でコピーするよりも高速になります)。
  • const T&の取得は、一時的な位置合わせのずれや間接的なコストのために遅くなる可能性があります。
14
Peter Alexander

呼び出し先と呼び出し元が別々のコンパイル単位で定義されている場合、コンパイラは参照を最適化することはできません。たとえば、次のコードをコンパイルしました。

#include <ctime>
#include <iostream>

int test1(int i);
int test2(const int& i);

int main() {
  int i = std::time(0);
  int j = test1(i);
  int k = test2(i);
  std::cout << j + k << std::endl;
}

最適化レベル3の64ビットLinuxでG ++を使用します。最初の呼び出しでは、メインメモリにアクセスする必要はありません。

call    time
movl    %eax, %edi     #1
movl    %eax, 12(%rsp) #2
call    _Z5test1i
leaq    12(%rsp), %rdi #3
movl    %eax, %ebx
call    _Z5test2RKi

行#1は、eaxの戻り値をeditest1の引数として直接使用します。 2行目と3行目は、結果をメインメモリにプッシュし、最初の引数にアドレスを配置します。これは、引数がintへの参照として宣言されているためです。そのアドレスを取る。レジスタを使用して何かを完全に計算できるか、メインメモリにアクセスする必要があるかは、最近大きな違いを生む可能性があります。したがって、入力するだけでなく、const int&も遅くなります。経験則では、最大でWordのサイズと同じ大きさのすべてのデータを値で渡し、それ以外はconstへの参照で渡します。また、constへの参照によってテンプレート化された引数を渡します。コンパイラはテンプレートの定義にアクセスできるため、参照を常に最適化できます。

7
Philipp

int &intは互換性がありません!特に、ローカルスタック変数への参照を返す場合、動作は未定義です。例:

int &func()
{
    int x = 42;
    return x;
}

あなたはcan関数の最後で破壊されないもの(例えば、静的またはクラスメンバー)への参照を返します。これは有効です:

int &func()
{
    static int x = 42;
    return x;
}

そして、外の世界に対して、intを直接返すのと同じ効果があります(ただし、これを変更できるので、const int & たくさん)。

参照の利点は、コピーが必要ないことです。これは、大きなクラスオブジェクトを扱う場合に重要です。ただし、多くの場合、コンパイラはそれを最適化できます。例参照 http://en.wikipedia.org/wiki/Return_value_optimization

7

「考える」のではなく、コンパイラによって最適化されます。アセンブラのリストを取得して、確実に見つけてみませんか。

junk.c ++:

int my_int()
{
    static int v = 5;
    return v;
}

const int& my_int_ref()
{
    static int v = 5;
    return v;
}

生成されたアセンブラー出力(省略):

_Z6my_intv:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    $5, %eax
    ret
    .cfi_endproc

...

_Z10my_int_refv:
.LFB1:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    $_ZZ10my_int_refvE1v, %eax
    ret

両方のmovl命令は非常に異なります。最初の動き5 into EAX(x86 Cコードで値を返すために伝統的に使用されるレジスタである)と2番目は変数のアドレス(明確にするために省略されている)をEAXに移動します。つまり、最初のケースの呼び出し関数は、メモリをヒットせずに直接レジスタ操作を使用して回答を使用できますが、2番目のケースでは、返されたポインタを介してメモリをヒットする必要があります。

そのため、最適化されていないようです。

これは、Tconst T&は交換できません。

intはdifferent with const int&です:

  1. const int&は別の整数変数(int B)への参照です。つまり、int Bを変更すると、const int&の値も変更されます。

2、intは別の整数変数(int B)の値のコピーです。つまり、int Bを変更しても、intの値は変更されません。

次のc ++コードを参照してください。

int main(){

ベクトルa {1,2,3};

int b = a [2]; //ベクトルが変わっても値は変わらない

const int&c = a [2]; //これは参照であるため、値はベクトルに依存します。

a [2] = 111;

// bは3を出力します;

// cは111を出力します;

}

0
Zhihui Shao