IT story

유사한 const 함수와 non-const 멤버 함수 간의 코드 복제를 어떻게 제거합니까?

hot-time 2020. 4. 24. 08:10
반응형

유사한 const 함수와 non-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
    }
};

두 멤버 함수 X::Z()X::Z() const괄호 안에 동일한 코드를 가지고있다. 이것은 중복 코드 이며 복잡한 논리를 가진 긴 기능에 대한 유지 보수 문제를 일으킬 수 있습니다 .

이 코드 복제를 피할 수있는 방법이 있습니까?


자세한 내용은 "복제 const및 비 const멤버 기능 피하기"페이지의 제목을 참조하십시오 . 23, 항목 3에서 " const가능할 때마다 사용 " , Scott Meyers가 제작 한 Effective C ++ , ISBN-13 : 9780321334879.

대체 텍스트

다음은 Meyers의 솔루션입니다 (간체).

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

두 캐스트와 함수 호출은 추악하지만 정확합니다. 메이어스는 이유를 철저히 설명합니다.


예, 코드 중복을 피할 수 있습니다. 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 ++ 표준 상태에서 정의되지 않은 동작이 발생합니다.


C ++ 11에서는 임시 도우미 기능을 사용하여 Scott Meyers의 솔루션을 향상시킬 수 있다고 생각합니다. 이것은 의도를 훨씬 더 분명하게 만들고 많은 다른 게터에게 재사용 할 수 있습니다.

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 포인터입니다. 두 번째는 호출 할 멤버 함수에 대한 포인터입니다. 그런 다음 임의의 추가 인수를 전달하여 함수로 전달할 수 있습니다. variadic 템플릿으로 인해 C ++ 11이 필요합니다.


C ++ 17은이 질문에 대한 최상의 답변을 업데이트했습니다.

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

이것은 다음과 같은 장점이 있습니다.

  • 무슨 일인지 분명하다
  • 최소한의 코드 오버 헤드-단일 라인에 적합
  • 잘못 이해하기 어렵습니다 ( volatile우연히 쫓겨날 수 있지만 volatile드문 예선 자입니다)

전체 공제 경로로 가고 싶다면 도우미 기능을 사용하여 수행 할 수 있습니다

template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
    return const_cast<T &>(value);
}
template<typename T>
constexpr T * as_mutable(T const * value) noexcept {
    return const_cast<T *>(value);
}
template<typename T>
constexpr T * as_mutable(T * value) noexcept {
    return value;
}
template<typename T>
void as_mutable(T const &&) = delete;

이제 엉망조차 할 수 없으며 volatile사용법은 다음과 같습니다.

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}

메이어보다 조금 더 장황하지만, 나는 이것을 할 수 있습니다 :

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 인스턴스에 대해 non-const Z &를 리턴하는 바람직하지 않은 특성을 가지므로 private입니다. 전용 메소드는 외부 인터페이스의 불변을 깰 수 있습니다 (이 경우 원하는 불변은 "const 객체는 그것을 통해 얻은 객체에 대한 참조를 통해 수정할 수 없습니다").

주석은 패턴의 일부입니다. _getZ의 인터페이스는 (접근자를 제외하고는) 그것을 호출하는 것이 결코 유효하지 않다고 명시합니다 : 어쨌든 그렇게하는 것에는 이점이 없습니다. 더 작거나 빠른 코드를 생성합니다. 메소드를 호출하는 것은 const_cast를 사용하여 접근 자 중 하나를 호출하는 것과 동일하며, 그렇게하고 싶지 않습니다. 오류를 명백하게하는 것에 대해 걱정이된다면 (그리고 그것은 공정한 목표입니다), _getZ 대신 const_cast_getZ를 호출하십시오.

그건 그렇고, Meyers의 솔루션에 감사드립니다. 나는 철학적으로 반대하지 않습니다. 개인적으로, 나는 약간의 제어 된 반복과 라인 노이즈처럼 보이는 방법에 대해 엄격하게 통제 된 특정 상황에서만 호출되어야하는 개인용 방법을 선호합니다. 독을 골라 내십시오.

[편집 : Kevin은 _getZ가 getZ와 같은 방식으로 const-specialized 된 추가 메소드 (예 : generateZ)를 호출 할 수 있다고 올바르게 지적했습니다. 이 경우 _getZ는 const Z &를보고 리턴하기 전에 const_cast해야합니다. 상용구 접근자가 모든 것을 감시하기 때문에 여전히 안전하지만 그것이 안전하다는 것은 눈에 띄지 않습니다. 또한 그렇게하면 나중에 const를 반환하도록 generateZ를 변경하면 항상 const를 반환하도록 getZ를 변경해야하지만 컴파일러는 그렇게 지시하지 않습니다.

컴파일러에 대한 후자의 점은 Meyers의 권장 패턴에도 해당되지만 명백하지 않은 const_cast에 대한 첫 번째 점은 그렇지 않습니다. 따라서 균형에서 _getZ가 반환 값으로 const_cast가 필요한 것으로 판명되면이 패턴은 Meyers보다 많은 가치를 잃습니다. Meyers와 비교할 때 단점이 있기 때문에 그 상황에서 나는 그를 전환 할 것이라고 생각합니다. 하나에서 다른 것으로 리팩토링하는 것은 쉽다. 유효하지 않은 코드와 상용구 만 _getZ를 호출하기 때문에 클래스의 다른 유효한 코드에는 영향을 미치지 않는다.]


좋은 질문과 좋은 답변. 캐스트를 사용하지 않는 다른 솔루션이 있습니다.

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 및 non-const 버전) 또는이 기능을 친구로 만들 수 있습니다. 그러나 나는 친구를 좋아하지 않습니다.

[편집 : 테스트 중에 추가 된 불필요한 cstdio 포함 제거]


논리를 개인 메소드로 옮기고 게터 내에서 "참조 및 리턴 얻기"만 수행하는 것은 어떻습니까? 실제로 간단한 getter 함수 내부의 정적 및 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

};

템플릿이나 캐스트만큼 멋지지는 않지만 의도 ( "이 두 함수는 동일해야 함")를 매우 명확하게 만듭니다.


그런 사람들을 위해

  • 사용 C ++ (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의 답변이 혼합되어 있습니다. 테이블에 추가하는 것은 단순히 함수의 이름을 지정하는 하나의 추가 코드 줄만으로 얻을 수 있다는 것입니다 (그러나 인수 또는 반환 유형 복제는 없음).

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 한정하기 위해 비슷한 접근법을 사용할 수 있으며 thisconst 오버로드 된 메소드를 호출 할 수 있습니다.

나머지는 올바른 접두사로 호출 앞에 접두사를 붙여 this->결과에서 const를 제거 하는 간단한 매크로입니다 . 매크로에 사용 된 표현식은 거의 항상 1 : 1 전달 인수를 사용하는 간단한 함수 호출이므로 여러 평가와 같은 매크로의 단점은 발생하지 않습니다. 줄임표와 함께 __VA_ARGS__사용할 수도 있지만 쉼표로 인해 필요하지 않아야합니다. 인수 구분 기호)는 괄호 안에 표시됩니다.

이 방법에는 몇 가지 이점이 있습니다.

  • 최소한의 자연스러운 구문 FROM_CONST_OVERLOAD( )
  • 추가 멤버 기능이 필요하지 않습니다
  • C ++ 98과 호환
  • 간단한 구현, 템플릿 메타 프로그래밍 및 종속성 없음
  • 확장 가능 : 다른 const 관계를 추가 할 수 있습니다 (예 const_iterator: std::shared_ptr<const T>, 등). 이를 WithoutConst()위해 해당 유형에 과부하가 걸리기 만하면 됩니다.

제한 사항 :이 솔루션은 비 const 오버로드가 const 오버로드와 정확히 동일한 작업을 수행하여 인수를 1 : 1로 전달할 수있는 시나리오에 최적화되어 있습니다. 논리가 다르고를 통해 const 버전을 호출하지 않으면 this->Method(args)다른 접근법을 고려할 수 있습니다.


일반적으로 const 및 non-const 버전이 필요한 멤버 함수는 getter 및 setter입니다. 대부분의 경우 그들은 하나의 라이너이므로 코드 복제는 문제가되지 않습니다.


나는 이것의 사용을 정당화 한 친구를 위해 이것을했다 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);
    }
};

이 DDJ 기사 는 const_cast를 사용할 필요가없는 템플릿 전문화 방법을 보여줍니다. 이러한 간단한 기능의 경우 실제로 필요하지 않습니다.

boost :: any_cast (한 번에 더 이상은 아닙니다) const가 아닌 버전을 호출하는 const 버전의 const_cast를 사용하여 중복을 피합니다. 비 const 버전에 const 의미를 부과 할 수는 없으므로 매우 주의해야합니다.

결국 두 개의 스 니펫이 서로 바로 위에있는 한 일부 코드 복제 괜찮습니다.


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

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메서드에 대한 참조 인수 는 템플릿에 대한 명백한 값별 인수와 일치하지 않으며 중단됩니다. 이유가 확실하지 않습니다. 이유는 다음과 같습니다 .

참고 URL : https://stackoverflow.com/questions/123758/how-do-i-remove-code-duplication-between-similar-const-and-non-const-member-func

반응형