유사한 const 함수와 non-const 멤버 함수 간의 코드 중복을 제거하려면 어떻게합니까? X { std::vector<Z> vecZ; public:

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


답변

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

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 포함 제거]