ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective C++] 4장(3) - 설계 및 선언
    쾌락없는 책임 (공부)/Effetive C++ 요약본 2022. 3. 31. 12:18
    반응형
    본 카테고리는 프로텍 미디어의 '이펙티브 C++'을 보고 요약하는 카테고리입니다.

    3판을 기준으로 하며 전체 내용이 아닌 간략한 내용만을 요약하고 있습니다.

     

    항목 24 : 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자

     책의 처음 부분에서는 클래스의 암시적 변환을 지원하는 건 안 좋은 생각이다라고 했었는데 이 규칙에도 예외가 있습니다. 바로 숫자 타입을 만들 때입니다.

    class Rational{
    public:
    	Rational (int numerator = 0, int denominator = 1);
    	int numerator() const;
    	int denominator() const;
    private:
    	...
    };

     유리수를 나타내는 클래스가 있을 때 이 클래스에 각종 연산을 해주고 싶은데 어떤 식으로 함수를 만들어야 할지 감이 잘 안 잡히죠. 일단 총 3개의 경우가 있습니다. 멤버 함수, 비멤버 함수, 비멤버 프랜드 함수로 만들 수 있겠죠. 어감상으로는 클래스 멤버로 두는 게 맞는 거 같은데 클래스와 관련한 함수를 그 클래스 멤버로 두는 것은 객체 지향 원리에 어긋나기 때문입니다.

    class Rational{
    public:
    	...
    	const Rational operator*(const Rational& rhs) const;
    };
    ...
    result = oneHalf * 2;  // no problem!
    result = 2 * oneHalf;  // ERROR!

     위 유리수 클래스에서 int 타입과 곱할 때 문제가 생기게 됩니다. int타입인 2에는 Rational 함수와 곱하는 operator가 없기 때문이죠. 위 oneHalf * 2 에서는 암시적 형변환을 통해서 문제없이 연산이 된 거고 아래는 순서상으로 형변환이 암시적으로 안된 것입니다. 

     동작도 일관적으로 하게 하고 에러도 안 나게 하려면 operator* 함수를 비멤버 함수로 만들어서 컴파일러 쪽에서 인자에 대해 암시적 타입 변환을 하게 하는 것

    const Rational operator* (const Rational& lhs, const Rational& rhs)
    {
    	return Rational(lhs.numerator() * rhs.numerator(), 
    					lhs.denominator() * rhs.denominator());
    }

     이렇게 하면 이제 오류 없이 사용할 수 있게 됩니다. 단 주의할 점은 프랜드 함수로 선언하면 안 됩니다. 일단 위 예제에서 정답은 '아니오'로 가급적 프랜드 함수를 만들지 않는 것을 추천드립니다.

     

     이번 항목에서는 여기서 끝나고 이후 탬플릿 함수를 사용할 때는 다른 식으로 사용하게 됩니다. 이후 나오는 항목 46을 참고해 봅시다.

     

     

     

    항목 25 : 예외를 던지지 않는 swap에 대한 지원도 생각해보자

     swap 함수는 초창기 stl 함수로 자기 대입 가능성에 대처하기 위한 대표적인 메커니즘으로 사랑받아 왔습니다. 일단 STL에 있는 swap 함수를 사용하는데 이게 구현된 모습은 아래와 비슷합니다.

    namespace std {
    	tmplate<typename T>
    	void swap(T& a, T& b){
    		T temp(a);
    		a = b;
    		b = temp;
    	}
    }

     그래서 복사 연산만 잘 지원한다면 어떤 타입의 객체든 맞바꾸기 동작을 잘 수행합니다. 그런데 막 멋있는 코드는 아니고 생으로 복사 연산을 하는 코드라 객체가 커지면 골치 아파집니다.

     

     그런 객체들은 실제 데이터를 가리키고 있는 포인터가 주성분인 타입들로 이런 개념을 사용한 기법이 pimpl이 있습니다.

     

    C++ pimpl과 관련한 이야기들

    개요  C++에 대한 이해도를 더 높여보기 위해서 Effective C++을 읽던 도중 pimpl 구조라는 흥미로운 키워드를 보게 되었습니다. 구현부는 따로 저장하고 이 객체의 주소를 가리키는 포인터를 사용하

    husk321.tistory.com

    class WidgetImpl{
    public:
    	...
    private:
    	int a, b, c;
    	std::vector<double> v;  // 아무튼 복사 비용이 높다!
    };
    
    // pimpl 관용구를 사용한 클래스
    class Widget{
    public:
    	Widget (const Widget& rhs);
    	Widget& operator = (const Widget& rhs)
    	{
    		...
    		*pImpl = *(rhs.pImpl);
    		...
    		// Widget을 복사하기 위해 자신의 WidgetImpl 객체를 복사
    	}
    private:
    // 실제 데이터를 가진 객체에 대한 포인터
    	WidgetImpl * pImpl;
    };

     pimpl로 감싸지 않았다면 원소를 다 복사해야 하지만 위 코드의 경우 복사 연산에서 포인터만 바꾸면 됩니다. 그래서 swap 함수도 pimpl로 바꿔주고 싶어 지죠.

    namespace std {
    	tmplate<>
    	void swap<Widget> (widget& a, Widget& b)
    	{
    		swap(a.pImpl, b.pImpl);
    	}
    }

     그런데 위 예시는 컴파일이 안됩니다. template< > 는 swap 중에서 탬플릿 완전 특수화라는 의미인데 왜 컴파일이 안될까요? 이는 단순히 포인터 멤버가 private라서 그렇습니다. 그래서 이런 특수화 함수를 프랜드로 선언할 수 있지만 이건 표준 템플릿들에 쓰인 규칙과 어긋나므로 하지 않는 게 좋습니다.

     

     그럼 다음 방법은 swap 함수를 public으로 멤버 함수를 선언하고 std::swap 함수의 특수화 함수에게 이 함수를 호출하게 하는 것입니다.

    class Widget{
    public:
    	...
    	void swap(Widget& other)
    	{
    		using std::swap;
    		
    		swap(pImpl, other.pImpl);
    	}
    	...
    };
    
    namespace std {
    	tmplate<>
    	void swap<Widget> (widget& a, Widget& b)
    	{
    		a.swap(b);
    	}
    }

     이러면 일단 컴파일도 되고 STL 컨테이너와 일관성도 유지해줄 수 있습니다. 그런데 Widget과 WidgetImpl 클래스가 아니라 클래스 탬플릿으로 만들어져 있어서 WidgetImpl에 저장된 데이터의 타입을 매개변수로 바꿀 수 있다면?

    template<typename T>
    class WidgetImpl {...};
    
    template<typename T>
    class Widget {...};

     swap 멤버 함수를 Widget에 넣는 것도 어렵지는 않지만 std::swap 함수를 특수화하는 게 좀 어렵습니다.

    namespace std{
    	template<typename T>
    	void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)  // ERROR!!!!
    	{
    		a.swap(b);
    	}
    }

     대충 이런 코드를 원하는데 실제로 C++에서는 받아주지 않습니다. 이런 경우 오버로드 버전을 추가할까 하는데 std는 규칙이 다소 특이해 적용하지 쉽지 않습니다.

     

    - std 내 템플릿에 대한 완전 특수화는 가능

    - 새 템플릿을 추가하는 건 안됨

     

      그래서 일단 추가를 하게 되면 '미정의 사항'이 되기 때문에 std 내 무언가를 추가하는걸 거의 금기시합니다.

     

     그럼 swap 함수에 템플릿 버전을 호출하게 하려면 어떻게 해야 할까요? 멤버 swap을 호출하는 비멤버 swap을 선언 하되 이 비멤버 함수를 std::swap의 특수화 버전이나 오버로딩 버전으로 선언하지만 않으면 됩니다. 다음과 같은 코드로 짜라는 이야기죠.

    namespace WidgetStuff {
    	...    // 템플릿으로 만든 WidgetImpl및 등등
    	template<typename T>
    	class Widget {...};                   // swap 란 이름의 멤버 함수가 들어 있음
    	...
    	template<typename T>                  // 비멤버 swap 함수
    	void swap(Widget<T>& a, Widget<T>& b)
    	{
    		a.swap(b);
    	}
    }

     이제 C++에서는 이름 규칙에 따라서 swap 함수를 찾을 것입니다. 한마디로 클래스 타입 전용의 swap 이 되도록 하고 싶다면 클래스와 동일한 네임스페이스 안에 비멤버 버전의 swap을 넣고 동시에 std::swap 함수의 특수화 버전을 만들면 됩니다.

     

     지금까지 내용은 전부 swap 구현자에 맞춰져 있었지만 이제 사용자 측면에서 보겠습니다. 여러분들이 함수 템플릿을 만들고 이후 swap 함수를 사용해 값을 바꿔보겠습니다. 이 부분에서 어떤 swap을 호출해야 할까요?

     

    - std의 일반 버전

    - std의 일반 버전을 특수화한 버전

    - T 타입 전용 버전

     

     여기서 T 타입 전용이 있으면 그걸 호출하고 없으면 일반이 호출되게 하고 싶으면 아래와 같이 작성하면 됩니다.

    tmplate<typename t>
    void DoSomething(T& obj1, T& obj2)
    {
    	using std::swap;
    	...
    	swap(obj1, obj2);
    }

     이러면 C++ 이 알아서 찾아두게 되는데 대신 주의할 점은 std::swap(obj1, obj2) 같이 한정자를 붙이는 일만 피하면 됩니다. 그러면 무조건 std의 일반 버전이 불려지게 되니깐요.

     


     이번 항목은 양이 많아 정리하는 시간이 있었습니다.

     

    1. 표준에서 제공하는 swap 이 효율이 떨어지는 거 같지 않으면 그냥 그대로 사용합니다.

    2. 표준 swap의 효율이 나쁘면 아래와 같이 합니다.

      - 두 객체의 값을 빠르게 바꾸는 함수를 구현하고 swap 이름을 붙인 뒤 public 멤버로 선언

      - 클래스(혹은 템플릿)가 들어 있는 네임스페이스와 같은 네임스페이스에 비멤버 swap 함수를 만들고 1에서 이를 호출하게 합니다.

      - 새 클래스를 만들고 있다면 그 클래스에 대한 swap의 특수화 버전을 준비해 둡시다. 그다음 swap을 호출하되 한정자는 붙이지 않게 합니다.

    3. 사용자 입장에서 swap 호출할 때 std::swap을 볼 수 있게 using을 사용합시다. 단 한정자는 붙이지 말고요.

     

     그리고 마무리하지 못한 이야기가 있는데 swap 함수는 강력한 예외 안전성을 보장하는 함수로 멤버 버전의 swap 함수가 예외를 던지지 않도록 해야 합니다. 이후 항목 29에서 볼 수 있을 겁니다.

    반응형

    댓글

Designed by Tistory.