서적 정리/Effective C++

33.상속된 이름을 숨기는 일은 피하자

민돌이2 2021. 12. 20. 19:18

유효범위(scope)는 변수의 수명을 의미한다. 지역변수, 전역변수 등 변수의 유효범위는 다르다.

만약 유효범위가 다른 변수의 이름이 같다면 어떤일이 발생할까?

int x; //전역 변수

void SomeFunc()
{
	int x; //지역 변수
	cin >> x; //지역 변수 x에 값을 넣음
}

컴파일러가 SomeFunc의 유혀범위 안에서 x라는 이름을 읽으면, 컴파일러는 자신이 처리하고 있는 유효범위를 찾아 같은 이름을 가진 것이 있는가 찾아본다. 현재 SomeFunc에서 x라는 이름이 있기 때문에 이 외의 유효범위에 대해서는 더이상 탐색하지 않는다.

즉, SomeFunc 함수의 지역변수 x에 의해  전역 변수 x는 이름이 가려진다.

 

상속에서로 주제를 다시 잡고 생각해보자.

기본 클래스에 속해 있는 것(멤버 함수, typedef 혹은 데이터 멤버)을 파생 클래스 멤버 함수 안에서 참조하는 문장이 있으면 컴파일러는 이 참조 대상을 바로 찾아낸다. 파생 클래스의 유효범위가 기본 클래스의 유효범위 안에 중첩되어 있기 때문이다.

class Base
{
public:
	virtual void mf1() = 0;
	virtual void mf2();
	void mf3();

private:
	int x;
};

class Derived : public Base
{
public:
	virtual void mf1();
	void mf4()
	{
		mf2();
	}
};

Drevied의 mf4 함수를 호출하면 mf2 함수를 만나게 된다. 컴파일러는 어떤 순서로 읽을까?

1.mf4 함수의 유효범위에서 mf2를 찾는다.

2.Derived 클래스의 유효범위에서 mf2를 찾는다.

3.Base 클래스의 유효범위에서 mf2를 찾는다.

4.Base를 둘러싸는 네임스페이스가 있다면 네임스페이스에서 mf2를 찾는다.

5.전역 유효범위에서 mf2를 찾는다.

이 경우에선 3번에서 mf2 함수를 찾고 호출하겠지만, 만약 전역 유효범위에서도 mf2 함수를 찾지 못한다면 링커는 에러를 표시할 것이다.

위의 순서가 C++의 이름 탐색 과정을 모두 설명한 것은 아니지만, 실제로 정확히 일어나는 과정이다.

 

만약 오버라이딩 + 오버로딩까지 섞여 있다면 어떻게 될까?

class Base
{
public:
	virtual void mf1() = 0; //오버로드
	virtual void mf1(int num); //오버로드
	virtual void mf2();
	void mf3(); //오버로드
	void mf3(int num); //오버로드

private:
	int x;
};

class Derived : public Base
{
public:
	virtual void mf1() override;
	void mf3(); //오버로드
	void mf4();
};

유효범위에 기반한 이름 가리기 규칙은 전혀 변한 것이 없기에, 기본 클래스의 mf1 및 mf3 함수는 파생 클래스의 mf1 및 mf3 함수에 의해 가려진다.

결론적으로 Base::mf1(int num)와 Base::mf3(int num)은 Drived가 상속한 것이 아니게 된다.

int main()
{
	Derived d;
	int x;

	d.mf1(); //문제x
	d.mf1(x); //에러

	d.mf2(); //문제x

	d.mf3(); //문제x
	d.mf3(x); //에러

	d.mf4(); //문제x

	system("Pause");
	return 0;
}

위 예제로 알 수 있는 것은 기본 클래스와 파생 클래스에 있는 이름이 같은 함수들이 받아들이는 매개변수 타입이 같든 다르든 비가상 함수, 가상 함수 상관없이 이름이 가려진다.

전적으로 이름 기준으로 가려진다. 이 점 때문에 변수든 함수 이름짓는게 골치썩을 때가 많다.

 

하지만 public 상속을 한다는 것은 기본 클래스의 모든 데이터를 쓰기 위함이다. 하지만 현재 파생 클래스는 기본 클래스의 오버로드한 함수는 사용하지 못하고 있기 때문에 모든 데이터를 쓰고 있다고 하기엔 하자가 있는것이 사실이다.

쉽게 풀어서 적으면 Derived 파생 클래스는 Base 기본 클래스의 Base::mf1(int num) 함수와 Base::mf3(int num) 함수를 사용하지 못하니 모든 Base 클래스를 사용하고 있다고 하기엔 문제가 있다는 것이다.

 

public 상속을 온전히 사용하기 위해서 using 선언을 하면 해결할 수 있다.

class Derived : public Base
{
public:
	using Base::mf1; //Derived 클래스 유효범위에 Base 클래스의 mf1까지 넣음
	using Base::mf3; //Derived 클래스 유효범위에 Base 클래스의 mf3까지 넣음

	...
};

Derived 클래스의 유효범위에 Base의 mf1 및 mf3 함수를 넣었다고 생각하면 쉽게 이해할 수 있을 것이다.

 

 

기본 클래스로 부터 파생 클래스가 모든 것을 상속하는 것을 원치 않을 때가 있다. public 상속하는 경우에는 애초에 파생 클래스 객체가 기본 클래스의 public 데이터를 외부에서 사용할 수 있으니 말이 안되긴 하지만, private 상속을 상속을 사용하는 경우엔 그럴법 한 경우다.

위 코드로 보면 mf1 함수의 매개변수가 있는 버전과 없는 버전으로 오버로드하고 있는데, 매개변수가 없는 버전만 상속하고 싶을 수 있다. using 선언을 하면 선언된 이름에 해당되는 것들이 모두 파생 클래스로 오기 때문에 불가능하다.

전달 함수(forwarding function)를 만들어 두면 가능하다.

class Base
{
public:
	virtual void mf1() = 0; //오버로드
	virtual void mf1(int num);
	...
};

class Derived : private Base
{
public:
	virtual void mf1() override
	{ Base::mf1(); } //Base 기본 클래스의 Base::mf1()를 호출함, 암시적 인라인 함수가 됨
	...
};

int main()
{
	Derived d;
	int x = 3;

	d.mf1(); //문제x
	d.mf1(x); //에러

	system("Pause");
	return 0;
}

Derived 클래스의 mf1함수는 암시적 인라인화 후보로 컴파일러가 올려둘 수 있다는 것 까지 생각이 닿는다면 베스트일 것이다.

 

이것만은 잊지 말자

파생 클래스의 이름은 기본 클래스의 이름을 가린다. public 상속에서는 이런 이름 가림 현상은 바람직하지 않다.

가려진 이름을 다시 볼 수 있게 하는 방법으로, using 선언 혹은 전달 함수를 쓸 수 있다.

728x90