同様の const メンバー関数と非 const メンバー関数間のコードの重複を削除するにはどうすればよいですか?
-
02-07-2019 - |
質問
次のものがあるとしましょう class X
内部メンバーにアクセスを返したい場合:
class Z
{
// details
};
class X
{
std::vector<Z> vecZ;
public:
Z& Z(size_t index)
{
// massive amounts of code for validating index
Z& ret = vecZ[index];
// even more code for determining that the Z instance
// at index is *exactly* the right sort of Z (a process
// which involves calculating leap years in which
// religious holidays fall on Tuesdays for
// the next thousand years or so)
return ret;
}
const Z& Z(size_t index) const
{
// identical to non-const X::Z(), except printed in
// a lighter shade of gray since
// we're running low on toner by this point
}
};
2 つのメンバー関数 X::Z()
そして X::Z() const
中括弧内は同一のコードです。これは重複したコードです 複雑なロジックを含む長い関数の場合、メンテナンスの問題が発生する可能性があります。.
このコードの重複を回避する方法はありますか?
解決 2
はい、コードの重複を避けることは可能です。const メンバー関数を使用してロジックを作成し、非 const メンバー関数で const メンバー関数を呼び出し、戻り値を非 const 参照 (関数がポインターを返す場合はポインター) に再キャストする必要があります。
class X
{
std::vector<Z> vecZ;
public:
const Z& Z(size_t index) const
{
// same really-really-really long access
// and checking code as in OP
// ...
return vecZ[index];
}
Z& Z(size_t index)
{
// One line. One ugly, ugly line - but just one line!
return const_cast<Z&>( static_cast<const X&>(*this).Z(index) );
}
#if 0 // A slightly less-ugly version
Z& Z(size_t index)
{
// Two lines -- one cast. This is slightly less ugly but takes an extra line.
const X& constMe = *this;
return const_cast<Z&>( constMe.Z(index) );
}
#endif
};
注記: そうすることが重要です ない ロジックを非 const 関数に配置し、const 関数が非 const 関数を呼び出すようにします。これにより、未定義の動作が発生する可能性があります。その理由は、定数クラスのインスタンスが非定数インスタンスとしてキャストされるためです。非 const メンバー関数は誤ってクラスを変更する可能性があり、C++ 標準では未定義の動作が発生すると規定されています。
他のヒント
詳細な説明については、見出し「重複を避ける」を参照してください。 const
そして非const
メンバー関数」(p.23、第3項「使用」 const
可能な限り、" 効果的な C++, 、Scott Meyers による 3D 版、ISBN-13:9780321334879。
マイヤーズの解決策(簡略化)は次のとおりです。
struct C {
const char & get() const {
return c;
}
char & get() {
return const_cast<char &>(static_cast<const C &>(*this).get());
}
char c;
};
2 つのキャストと関数呼び出しは見にくいかもしれませんが、正しいです。マイヤーズ氏はその理由を徹底的に説明している。
Scott Meyers のソリューションは、テンプレート ヘルパー関数を使用することで C++11 で改善できると思います。これにより、意図がより明確になり、他の多くのゲッターで再利用できるようになります。
template <typename T>
struct NonConst {typedef T type;};
template <typename T>
struct NonConst<T const> {typedef T type;}; //by value
template <typename T>
struct NonConst<T const&> {typedef T& type;}; //by reference
template <typename T>
struct NonConst<T const*> {typedef T* type;}; //by pointer
template <typename T>
struct NonConst<T const&&> {typedef T&& type;}; //by rvalue-reference
template<typename TConstReturn, class TObj, typename... TArgs>
typename NonConst<TConstReturn>::type likeConstVersion(
TObj const* obj,
TConstReturn (TObj::* memFun)(TArgs...) const,
TArgs&&... args) {
return const_cast<typename NonConst<TConstReturn>::type>(
(obj->*memFun)(std::forward<TArgs>(args)...));
}
このヘルパー関数は次のように使用できます。
struct T {
int arr[100];
int const& getElement(size_t i) const{
return arr[i];
}
int& getElement(size_t i) {
return likeConstVersion(this, &T::getElement, i);
}
};
最初の引数は常に this ポインタです。2 番目は、呼び出すメンバー関数へのポインターです。その後、関数に転送できるように、任意の量の追加の引数を渡すことができます。可変個引数テンプレートがあるため、これには C++11 が必要です。
マイヤーズよりも少し冗長ですが、私は次のようにするかもしれません。
class X {
private:
// This method MUST NOT be called except from boilerplate accessors.
Z &_getZ(size_t index) const {
return something;
}
// boilerplate accessors
public:
Z &getZ(size_t index) { return _getZ(index); }
const Z &getZ(size_t index) const { return _getZ(index); }
};
プライベート メソッドには、const インスタンスに対して非 const Z& を返すという望ましくない特性があるため、プライベート メソッドとなります。プライベート メソッドは、外部インターフェイスの不変条件を壊す可能性があります (この場合、望ましい不変条件は、「const オブジェクトは、それが持つオブジェクトへの参照を介して取得された参照を介して変更できない」です)。
コメントはパターンの一部であることに注意してください。_getZ のインターフェイスは、(アクセサを除いて) それを呼び出すことが決して有効ではないことを指定しています。いずれにせよ、そうすることによる利点は考えられません。入力する文字が 1 文字増えるだけで、コードが小さくなったり速くなったりするわけではないからです。このメソッドを呼び出すことは、const_cast を使用してアクセサーの 1 つを呼び出すことと同じであり、それも行いたくないでしょう。エラーを明白にすることが心配な場合 (それは正当な目標です)、_getZ ではなく const_cast_getZ と呼んでください。
ところで、私はマイヤーズの解決策に感謝しています。私はそれに対して哲学的に異論はありません。ただし、個人的には、回線ノイズのようなメソッドよりも、少しだけ制御された繰り返しや、厳密に制御された特定の状況でのみ呼び出す必要があるプライベート メソッドの方が好きです。毒を選んでそれを飲み続けてください。
[編集:Kevin は、_getZ が getZ と同じように const に特化したさらなるメソッド (たとえば、generateZ) を呼び出したい可能性があることを正しく指摘しました。この場合、_getZ は const Z& を認識し、戻る前にそれを const_cast する必要があります。ボイラープレート アクセサーがすべてを監視するため、それでも安全ですが、安全かどうかはそれほど明らかではありません。さらに、これを行った後、常に const を返すようにgenerateZ を変更した場合、常に const を返すように getZ も変更する必要がありますが、コンパイラはそのことを通知しません。
コンパイラに関する後者の点は、Meyers の推奨パターンにも当てはまりますが、自明ではない const_cast に関する最初の点は当てはまりません。したがって、総合的に考えて、_getZ が戻り値に const_cast を必要とすることが判明した場合、このパターンは Meyers のパターンよりも多くの価値を失うと思います。マイヤーズと比べるとデメリットもあるので、その場合は彼のほうに乗り換えると思います。一方から他方へのリファクタリングは簡単です。無効なコードのみが存在し、ボイラープレートが _getZ を呼び出すため、クラス内の他の有効なコードには影響しません。]
C++17 は、この質問に対する最良の回答を更新しました。
T const & f() const {
return something_complicated();
}
T & f() {
return const_cast<T &>(std::as_const(*this).f());
}
これには次のような利点があります。
- 何が起こっているかは明らかです
- コードのオーバーヘッドが最小限に抑えられ、1 行に収まります
- 間違えにくい(投げ捨てることしかできない)
volatile
偶然ですが、volatile
は珍しい修飾子です)
完全な控除ルートを実行したい場合は、ヘルパー関数を使用することで実現できます。
template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
return const_cast<T &>(value);
}
template<typename T>
void as_mutable(T const &&) = delete;
今では台無しにすることさえできません volatile
, 、使用法は次のようになります
T & f() {
return as_mutable(std::as_const(*this).f());
}
素敵な質問と素敵な答え。キャストを使用しない別の解決策があります。
class X {
private:
std::vector<Z> v;
template<typename InstanceType>
static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) {
// massive amounts of code for validating index
// the instance variable has to be used to access class members
return instance.v[i];
}
public:
const Z& get(std::size_t i) const {
return get(*this, i);
}
Z& get(std::size_t i) {
return get(*this, i);
}
};
ただし、静的メンバーを必要とし、 instance
その中の変数。
この解決策がもたらす可能性のある (マイナスの) 影響をすべて考慮したわけではありません。もしあれば教えてください。
テンプレートを使用してこれを解決することもできます。この解決策は少し醜いですが (ただし、その醜さは .cpp ファイルに隠されています)、コンパイラによる定数性チェックが提供され、コードの重複はありません。
.h ファイル:
#include <vector>
class Z
{
// details
};
class X
{
std::vector<Z> vecZ;
public:
const std::vector<Z>& GetVector() const { return vecZ; }
std::vector<Z>& GetVector() { return vecZ; }
Z& GetZ( size_t index );
const Z& GetZ( size_t index ) const;
};
.cpp ファイル:
#include "constnonconst.h"
template< class ParentPtr, class Child >
Child& GetZImpl( ParentPtr parent, size_t index )
{
// ... massive amounts of code ...
// Note you may only use methods of X here that are
// available in both const and non-const varieties.
Child& ret = parent->GetVector()[index];
// ... even more code ...
return ret;
}
Z& X::GetZ( size_t index )
{
return GetZImpl< X*, Z >( this, index );
}
const Z& X::GetZ( size_t index ) const
{
return GetZImpl< const X*, const Z >( this, index );
}
私が知る主な欠点は、メソッドの複雑な実装がすべてグローバル関数内にあるため、上記の GetVector() のようなパブリック メソッドを使用して X のメンバーを取得する必要があることです (このメソッドには常にconst バージョンと非 const バージョン)、またはこの関数を友達にすることもできます。でも友達は好きじゃない。
[編集:テスト中に追加された cstdio の不要なインクルードを削除しました。]
ロジックをプライベート メソッドに移動し、ゲッター内で「参照を取得して返す」ことだけを行うのはどうでしょうか。実際、私は単純な getter 関数内の static キャストと const キャストについてかなり混乱するでしょうし、非常にまれな状況を除いて、それは醜いと思います。
プリプロセッサを使用するのは不正行為ですか?
struct A {
#define GETTER_CORE_CODE \
/* line 1 of getter code */ \
/* line 2 of getter code */ \
/* .....etc............. */ \
/* line n of getter code */
// ^ NOTE: line continuation char '\' on all lines but the last
B& get() {
GETTER_CORE_CODE
}
const B& get() const {
GETTER_CORE_CODE
}
#undef GETTER_CORE_CODE
};
これはテンプレートやキャストほど派手ではありませんが、意図 (「これら 2 つの関数は同一である」) をかなり明示的にします。
通常、const バージョンと非 const バージョンが必要なメンバー関数はゲッターとセッターです。ほとんどの場合、これらはワンライナーであるため、コードの重複は問題になりません。
の使用を正当に正当化した友人のためにこれを行いました。 const_cast
...それを知らなかったら、おそらく次のようなことをしていたでしょう(あまりエレガントではありません):
#include <iostream>
class MyClass
{
public:
int getI()
{
std::cout << "non-const getter" << std::endl;
return privateGetI<MyClass, int>(*this);
}
const int getI() const
{
std::cout << "const getter" << std::endl;
return privateGetI<const MyClass, const int>(*this);
}
private:
template <class C, typename T>
static T privateGetI(C c)
{
//do my stuff
return c._i;
}
int _i;
};
int main()
{
const MyClass myConstClass = MyClass();
myConstClass.getI();
MyClass myNonConstClass;
myNonConstClass.getI();
return 0;
}
次のようなプライベート ヘルパー静的関数テンプレートをお勧めします。
class X
{
std::vector<Z> vecZ;
// ReturnType is explicitly 'Z&' or 'const Z&'
// ThisType is deduced to be 'X' or 'const X'
template <typename ReturnType, typename ThisType>
static ReturnType Z_impl(ThisType& self, size_t index)
{
// massive amounts of code for validating index
ReturnType ret = self.vecZ[index];
// even more code for determining, blah, blah...
return ret;
}
public:
Z& Z(size_t index)
{
return Z_impl<Z&>(*this, index);
}
const Z& Z(size_t index) const
{
return Z_impl<const Z&>(*this, index);
}
};
(私と同じように)
- 使用 C++17
- を追加したい 最小限の定型文/繰り返しと
- 使っても構いません マクロ (メタクラスを待っている間...)、
ここに別のテイクがあります:
#include <utility>
#include <type_traits>
template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};
#define NON_CONST(func) \
template <typename... T> \
auto func(T&&... a) -> typename NonConst<decltype(func(a...))>::type { \
return const_cast<decltype(func(a...))>( \
std::as_const(*this).func(std::forward<T>(a)...)); \
}
基本的には、@Pait、@DavidStone、@sh1 からの回答を組み合わせたものです。これによりテーブルに追加されるのは、関数に名前を付けるだけのコードを 1 行追加するだけで済むことです (ただし、引数や戻り値の型は重複しません)。
class X
{
const Z& get(size_t index) const { ... }
NON_CONST(get)
};
注記:gcc は 8.1 より前ではこれをコンパイルできませんでしたが、clang-5 以降および MSVC-19 は問題ありません (によると) コンパイラエクスプローラー).
非常に多くの異なる答えがあるにもかかわらず、ほとんどすべてが強力なテンプレートの魔法に依存していることに私は驚きました。テンプレートは強力ですが、簡潔さの点ではマクロの方が優れている場合があります。多くの場合、両方を組み合わせることで最大限の汎用性が得られます。
マクロを書きました FROM_CONST_OVERLOAD()
これを非 const 関数に配置して const 関数を呼び出すことができます。
使用例:
class MyClass
{
private:
std::vector<std::string> data = {"str", "x"};
public:
// Works for references
const std::string& GetRef(std::size_t index) const
{
return data[index];
}
std::string& GetRef(std::size_t index)
{
return FROM_CONST_OVERLOAD( GetRef(index) );
}
// Works for pointers
const std::string* GetPtr(std::size_t index) const
{
return &data[index];
}
std::string* GetPtr(std::size_t index)
{
return FROM_CONST_OVERLOAD( GetPtr(index) );
}
};
シンプルで再利用可能な実装:
template <typename T>
T& WithoutConst(const T& ref)
{
return const_cast<T&>(ref);
}
template <typename T>
T* WithoutConst(const T* ptr)
{
return const_cast<T*>(ptr);
}
template <typename T>
const T* WithConst(T* ptr)
{
return ptr;
}
#define FROM_CONST_OVERLOAD(FunctionCall) \
WithoutConst(WithConst(this)->FunctionCall)
説明:
多くの回答に投稿されているように、非 const メンバー関数でのコードの重複を避けるための一般的なパターンは次のとおりです。
return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );
この定型文の多くは、型推論を使用して回避できます。初め、 const_cast
にカプセル化できます WithoutConst()
, 、引数の型を推論し、const 修飾子を削除します。次に、同様のアプローチを次の場合にも使用できます。 WithConst()
を const 修飾する this
ポインター。const オーバーロードされたメソッドの呼び出しを可能にします。
残りは、呼び出しに正しく修飾された接頭辞を付ける単純なマクロです。 this->
そして結果から const を削除します。マクロで使用される式は、ほとんどの場合、1:1 で転送された引数を持つ単純な関数呼び出しであるため、複数の評価などのマクロの欠点は影響しません。省略記号と __VA_ARGS__
も使用できますが、括弧内にカンマ (引数の区切り文字として) が含まれるため、必要ありません。
このアプローチにはいくつかの利点があります。
- 最小限で自然な構文 -- 呼び出しをラップするだけです
FROM_CONST_OVERLOAD( )
- 追加のメンバー関数は必要ありません
- C++98と互換性あり
- シンプルな実装、テンプレートのメタプログラミングなし、依存関係なし
- 拡張可能:他の const 関係を追加することもできます (例:
const_iterator
,std::shared_ptr<const T>
, 、など)。このためには、単純にオーバーロードしますWithoutConst()
対応するタイプの場合。
制限事項:このソリューションは、引数を 1:1 で転送できるように、非 const オーバーロードが const オーバーロードとまったく同じ動作をするシナリオ向けに最適化されています。ロジックが異なり、const バージョンを呼び出していない場合は、 this->Method(args)
, 、他のアプローチを検討することもできます。
このDDJの記事 は、const_cast を使用する必要のないテンプレートの特殊化を使用する方法を示しています。ただし、このような単純な関数の場合、実際には必要ありません。
boost::any_cast (ある時点ではもう使用しません) は、重複を避けるために const バージョンの const_cast を使用して非 const バージョンを呼び出します。ただし、非 const バージョンに const セマンティクスを強制することはできないため、次のようにする必要があります。 とても それには注意してください。
最終的にはコードの重複がいくつかあります は 2 つのスニペットが直接重なっていれば問題ありません。
jwfearn と kevin が提供したソリューションに追加するために、関数がshared_ptrを返した場合の対応するソリューションを次に示します。
struct C {
shared_ptr<const char> get() const {
return c;
}
shared_ptr<char> get() {
return const_pointer_cast<char>(static_cast<const C &>(*this).get());
}
shared_ptr<char> c;
};
探していたものが見つからなかったので、自分でいくつか巻いてみました...
これは少し冗長ですが、同じ名前 (および戻り値の型) の多数のオーバーロードされたメソッドを一度に処理できるという利点があります。
struct C {
int x[10];
int const* getp() const { return x; }
int const* getp(int i) const { return &x[i]; }
int const* getp(int* p) const { return &x[*p]; }
int const& getr() const { return x[0]; }
int const& getr(int i) const { return x[i]; }
int const& getr(int* p) const { return x[*p]; }
template<typename... Ts>
auto* getp(Ts... args) {
auto const* p = this;
return const_cast<int*>(p->getp(args...));
}
template<typename... Ts>
auto& getr(Ts... args) {
auto const* p = this;
return const_cast<int&>(p->getr(args...));
}
};
1つだけ持っている場合 const
名前ごとにメソッドを追加しますが、それでも複製するメソッドがたくさんある場合は、次のようにすることをお勧めします。
template<typename T, typename... Ts>
auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) {
return const_cast<T*>((this->*f)(args...));
}
int* getp_i(int i) { return pwrap(&C::getp_i, i); }
int* getp_p(int* p) { return pwrap(&C::getp_p, p); }
残念ながら、これは名前のオーバーロードを開始するとすぐに失敗します (関数ポインター引数の引数リストはその時点では未解決であるようで、関数の引数に一致するものを見つけることができません)。ただし、テンプレートを使用してそこから抜け出すこともできます。
template<typename... Ts>
auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }
ただし、参照引数 const
メソッドは、テンプレートへの明らかに値渡しの引数との照合に失敗し、中断されます。 理由はわかりません。その理由は次のとおりです.