web-dev-qa-db-ja.com

類似のconstメンバー関数と非constメンバー関数間のコード重複を削除するにはどうすればよいですか?

次の_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は、中括弧内に同じコードがあります。これは重複したコードですそして複雑なロジックを持つ長い関数の保守問題を引き起こす可能性があります

このコードの重複を避ける方法はありますか?

216
Kevin

はい、コードの重複を避けることができます。 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
};

注:行うことが重要です[〜#〜] not [〜#〜] non-const関数にロジックを配置し、const-functionが非const関数-未定義の動作を引き起こす可能性があります。理由は、定数クラスインスタンスが非定数インスタンスとしてキャストされるためです。非constメンバ関数はクラスを誤って変更する場合があり、C++標準では未定義の動作になります。

59
Kevin

詳細な説明については、「constおよびNon _constメンバー関数での重複の回避」の見出しを参照してください。 23、項目3「可能な限りconstを使用」、 Effective C++、3d ed Scottマイヤーズ、ISBN-13:9780321334879。

alt text

Meyersのソリューション(簡略化)は次のとおりです。

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

2つのキャストと関数呼び出しは見苦しいかもしれませんが、正しいです。マイヤーズはその理由を徹底的に説明しています。

168
jwfearn

Scott Meyersのソリューションは、tempateヘルパー関数を使用することで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-pointerです。 2番目は、呼び出すメンバー関数へのポインターです。その後、関数に転送できるように、任意の量の追加引数を渡すことができます。可変個のテンプレートのため、これにはC++ 11が必要です。

32
Pait

マイヤーズよりも少し冗長ですが、私はこれを行うかもしれません:

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); }
};

Privateメソッドには、constインスタンスに対して非const Z&を返すという望ましくないプロパティがあります。そのため、このメソッドはprivateです。プライベートメソッドは、外部インターフェイスの不変式を壊す可能性があります(この場合、望ましい不変式は、「constオブジェクトは、それを通じて取得されたオブジェクトへの参照を介して変更できません」です)。

コメントはパターンの一部であることに注意してください-_getZのインターフェースは、それを呼び出すことは決して有効ではないことを指定します(明らかにアクセサーを除く):とにかくそうすることは考えられない利点ですその結果、コードがより小さくまたはより高速になります。メソッドを呼び出すことは、const_castを使用してアクセサーの1つを呼び出すことと同等であり、それも行いたくないでしょう。エラーを明らかにすることを心配している場合(それが公正な目標です)、_ getZではなくconst_cast_getZを呼び出します。

ところで、マイヤーズのソリューションに感謝します。哲学的な異議はありません。しかし、個人的には、制御された繰り返しのわずかな部分と、ラインノイズのように見えるメソッドよりも、厳密に制御された特定の状況でのみ呼び出されるプライベートメソッドを好みます。あなたの毒を選んでそれに固執する。

[編集:Kevinは_getZがgetZと同じようにconstに特化された別のメソッド(たとえばgenerateZ)を呼び出したいかもしれないことを正しく指摘しました。この場合、_getZはconst Z&を参照し、戻る前にconst_castする必要があります。ボイラープレートアクセサーがすべてをポリシングするので、それはまだ安全ですが、安全であることは際立って明白ではありません。さらに、それを行ってからgenerateZを変更して常にconstを返すようにした場合、getZを変更して常にconstを返すようにする必要がありますが、コンパイラーはそのことを通知しません。

コンパイラに関する後者の点は、マイヤーズの推奨パターンにも当てはまりますが、const_castが明白でない点に関する最初の点はそうではありません。したがって、バランス上、_getZが戻り値にconst_castを必要とすると、このパターンはMeyersのものよりも多くの値を失うと思います。また、マイヤーズに比べて不利な点があるため、その状況で彼に切り替えると思います。一方から他方へのリファクタリングは簡単です。無効なコードとボイラープレートのみが_getZを呼び出すため、クラス内の他の有効なコードには影響しません。]

20
Steve Jessop

C++ 17は、この質問に対するベストアンサーを更新しました。

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

これには次の利点があります。

  • 何が起こっているのか明らかです
  • コードのオーバーヘッドが最小限です-1行に収まります
  • 間違いを犯すのは難しい(偶然volatileを捨てることしかできないが、volatileはまれな修飾子である)

あなたが完全な演routeルートに行きたいなら、それはヘルパー機能を持つことによって達成することができます

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());
}
19
David Stone

いい質問といい答え。キャストを使用しない別のソリューションがあります:

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変数を使用する必要があるというneedさを持っています。

私はこの解決策のすべての可能な(否定的な)意味を考慮しませんでした。もしあれば教えてください。

17
gd1

テンプレートでこれを解決することもできます。この解決策はややいですが(しかし、さは.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の不要なインクルードを削除。]

6
Andy Balaam

ロジックをプライベートメソッドに移動し、ゲッター内で「参照を取得して返す」ことだけを行うのはどうですか。実際、私は単純なゲッター関数内の静的キャストとconstキャストについてかなり混乱しているので、非常にまれな状況を除いて、そのugいことを考えます!

3
MP24

(私のような)人のために

  • usec ++ 17
  • 最小量のボイラープレート/繰り返しを追加したい
  • makros(メタクラスを待っている間に...)

ここに別のテイクがあります:

#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は満足です( コンパイラエクスプローラー による)。

1
axxel

プリプロセッサを使用するのはごまかしですか?

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つの関数は同一である」)。

1
user1476176

驚くほど多くの異なる答えがありますが、ほとんどすべてが重いテンプレートマジックに依存しています。テンプレートは強力ですが、時にはマクロが簡潔にそれらを打ち負かします。多くの場合、両方を組み合わせることで最大限の汎用性が得られます。

Const関数を呼び出すために非const関数に配置できるマクロFROM_CONST_OVERLOAD()を作成しました。

使用例:

_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-qualifierが削除されます。次に、thisポインターをconst修飾するためにWithConst()で同様のアプローチを使用できます。これにより、const-overloadedメソッドの呼び出しが可能になります。

残りは、呼び出しの前に正しく修飾された_this->_を付け、結果からconstを削除する単純なマクロです。マクロで使用される式は、ほとんどの場合、1対1の転送引数を使用した単純な関数呼び出しであるため、多重評価などのマクロの欠点は発生しません。Ellipsisおよび___VA_ARGS___も使用できますが、使用しないでください(引数の区切りとして)コンマが括弧内にあるため、必要です。

このアプローチにはいくつかの利点があります。

  • 最小限の自然な構文-呼び出しをFROM_CONST_OVERLOAD( )でラップするだけです
  • 追加のメンバー関数は不要
  • C++ 98との互換性
  • シンプルな実装、テンプレートメタプログラミングなし、依存関係ゼロ
  • 拡張可能:他のconst関係を追加できます(_const_iterator_、_std::shared_ptr<const T>_など)。このためには、対応する型のWithoutConst()を単純にオーバーロードします。

制限:このソリューションは、非constオーバーロードがconstオーバーロードとまったく同じように動作するシナリオ向けに最適化されているため、引数を1:1で転送できます。ロジックが異なり、this->Method(args)を介してconstバージョンを呼び出さない場合は、他のアプローチを検討できます。

1
TheOperator

通常、constおよびnon-constバージョンが必要なメンバー関数は、ゲッターとセッターです。ほとんどの場合、それらはワンライナーなので、コードの重複は問題になりません。

0
Dima

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;
};
0
Christer Swahn

次のようなプライベートヘルパーの静的関数テンプレートをお勧めします。

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);
    }
};
0
dats

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;
}
0
matovitch

私が探していたものが見つからなかったので、私は自分のいくつかを転がしました...

これは少し冗長ですが、同じ名前(および戻り値の型)の多数のオーバーロードメソッドを一度に処理できるという利点があります。

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...));
  }
};

名前ごとにconstメソッドが1つだけあり、それでも複製するメソッドがたくさんある場合は、これを好むかもしれません。

  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メソッドへの参照引数は、テンプレートへの明らかに値による引数との照合に失敗し、壊れます。 理由はわかりません。その理由

0
sh1