私は通常、戻り値または引数として定数参照を使用していることに気付きました。その理由は、コードで非参照を使用するのとほぼ同じように機能するからだと思います。しかし、間違いなくより多くのスペースが必要になり、関数宣言が長くなります。私はそのようなコードで大丈夫ですが、一部の人々はそれが悪いプログラミングスタイルだと思うと思います。
どう思いますか? const int& over intと書く価値はありますか?とにかくコンパイラによって最適化されていると思うので、コーディングに時間を浪費しているだけかもしれません。
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はプログラマーを助けるためだけに存在する-私は個人的にそれがそんなに大きな助けになるかどうかはわかりませんが、それは別の話です)。
Oliが言うように、T
とは対照的にconst T&
を返すことは完全に異なるものであり、特定の状況で壊れる可能性があります(彼の例のように)。
引数としてプレーンT
とは対照的にconst T&
を使用することは、物事を壊す可能性は低いですが、それでもいくつかの重要な違いがあります。
const T&
の代わりにT
を使用するには、T
がコピー構築可能であることが必要です。T
を取得すると、コピーコンストラクターが呼び出されますが、コピーコンストラクターは高価になる可能性があります(また、関数終了時のデストラクタも)。T
を使用すると、パラメーターをローカル変数として変更できます(手動でコピーするよりも高速になります)。const T&
の取得は、一時的な位置合わせのずれや間接的なコストのために遅くなる可能性があります。呼び出し先と呼び出し元が別々のコンパイル単位で定義されている場合、コンパイラは参照を最適化することはできません。たとえば、次のコードをコンパイルしました。
#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
の戻り値をedi
のtest1
の引数として直接使用します。 2行目と3行目は、結果をメインメモリにプッシュし、最初の引数にアドレスを配置します。これは、引数がintへの参照として宣言されているためです。そのアドレスを取る。レジスタを使用して何かを完全に計算できるか、メインメモリにアクセスする必要があるかは、最近大きな違いを生む可能性があります。したがって、入力するだけでなく、const int&
も遅くなります。経験則では、最大でWordのサイズと同じ大きさのすべてのデータを値で渡し、それ以外はconstへの参照で渡します。また、constへの参照によってテンプレート化された引数を渡します。コンパイラはテンプレートの定義にアクセスできるため、参照を常に最適化できます。
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 。
「考える」のではなく、コンパイラによって最適化されます。アセンブラのリストを取得して、確実に見つけてみませんか。
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番目のケースでは、返されたポインタを介してメモリをヒットする必要があります。
そのため、最適化されていないようです。
これは、T
とconst T&
は交換できません。
intはdifferent with 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を出力します;
}