Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

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

책/Effective C++

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

아롱로또 2023. 12. 1. 13:33

다루는 내용

C++의 이름 가리기 규칙을 알아보고 이를 원치 않을 때 해결할 수 있는 방안에 대해 다룬다.

유효 범위(Scope)와 이름 가리기

int x; // 전역 변수
int main(){ 
	double x; // 지역 변수
	cin >> x; // 지역 변수 사용
}

전역변수 int x는 main에서 유효하지만 main에서 선언된 double x에 의해 이름이 가려진다.

따라서 cin >> x 의 x는 main의 double x이다.

 

컴파일러가 main의 유효 범위 안에서 x라는 이름을 만나면 가장 먼저 자신이 처리하고 있는 유효범위, 즉 지역 유효범위를 뒤져서 동일한 이름을 가진 것을 찾아본다. x라는 이름을 찾았기 때문에 더 이상 탐색하지 않는다.

이러한 C++의 이름 가리기 규칙은 타입에 상관없이 이름을 가려버린다.

 

상속과 이름 가리기

#include <iostream>
using namespace std;

class Base{
public:
	virtual void mf1() = 0;
	virtual void mf2(){ cout << "Base::mf2()" << endl; }
	void mf3(){ cout << "Base::mf3()" << endl; }
};

class Derived: public Base{
public:
	virtual void mf1(){ cout << "Derived::mf1()" << endl; }
	void mf4(){
		cout << "Derived::mf4()" << endl;
		mf2();
	}
};

int main(){
	Derived d;
	d.mf1();
	d.mf4();
	return 0;
}

Derived 클래스는 Base 클래스를 상속하여 mf1 함수를 재정의하고, mf4 함수를 추가하여 mf2 함수를 호출한다.

main 함수를 실행하면 mf1()과 mf4()는 모두 Derived 클래스에 존재하므로 각각 Derived::mf1(), Dervied::mf4()를 호출할 것이다.

Derived::mf4()를 호출한 컴파일러는 mf2() 를 발견한다. 이 때 컴파일러의 유효범위 탐색은 다음과 같다.

  1. 컴파일러는 지역 유효범위(mf4의 유효 범위)에서 mf2()를 찾는다.
  2. Derived 클래스에서 mf2()를 찾는다.
  3. Derived 클래스를 감싸고 있는 Base 클래스에서 찾는다.

이러한 과정을 거친 실행 결과는 다음과 같다.

실행 결과

 

상속과 이름 가리기 2

#include <iostream>
using namespace std;

class Base{
public:
	virtual void mf1() = 0;
	virtual void mf1(int v){ cout << "Base::mf1(int): " << v << endl; }
	virtual void mf2(){ cout << "Base::mf2()" << endl; }
	void mf3(){ cout << "Base::mf3()" << endl; }
	virtual void mf3(double d){ cout << "Base::mf1(double): " << d << endl; }
};

class Derived: public Base{
public:
	virtual void mf1(){ cout << "Derived::mf1()" << endl; }
	void mf3(){ cout << "Derived::mf3()" << endl; }
	void mf4(){ cout << "Derived::mf4()" << endl; }
};

int main(){
	Derived d;
	int x = 1234;
	d.mf1(); // Derived::mf1()
	d.mf1(x); // Error
	d.mf2(); // Base::mf2()
	d.mf3(); // Derived::mf3()
	d.mf3(x); // Error
	return 0;
}

 

위 코드는 main 함수의 주석을 보다시피, 컴파일 에러가 발생할 것이다. 이유는 다음과 같다.

컴파일 에러 원인

 

처음에 존재하는 "유효 범위(Scope)와 이름 가리기" 에서 이름 가리기 규칙은 타입에 상관없이 이름을 가린다고 했다.

따라서, Base::mf1(), Base::mf1(int), Base::mf3()는 Derived::mf1()과 Derived::mf3()에 의해 타입 상관없이 가려진다.

매개변수 타입이 다르든, virtual 키워드를 사용했든 상관없이 이름을 가린다.

 

이러한 동작 방법은 우리가 어떤 라이브러리 혹은 프레임워크를 이용하여 파생 클래스를 만들 때, 멀리 떨어진 기본 클래스로부터 오버로드 버전을 상속시키는 경우를 막기 위해 설계되었다. 

 

C++이 기본적으로 제공하는 상속된 이름 가리기를 무시하기 위해서는 using 선언을 사용할 수 있다.

#include <iostream>
using namespace std;

class Base{
public:
	virtual void mf1() = 0;
	virtual void mf1(int v){ cout << "Base::mf1(int): " << v << endl; }
	virtual void mf2(){ cout << "Base::mf2()" << endl; }
	void mf3(){ cout << "Base::mf3()" << endl; }
	virtual void mf3(double d){ cout << "Base::mf1(double): " << d << endl; }
};

class Derived: public Base{
public:
	using Base::mf1;
	using Base::mf3;
	
	virtual void mf1(){ cout << "Derived::mf1()" << endl; }
	void mf3(){ cout << "Derived::mf3()" << endl; }
	void mf4(){ cout << "Derived::mf4()" << endl; }
};

int main(){
	Derived d;
	int x = 1234;
	d.mf1(); // Derived::mf1()
	d.mf1(x); // Error
	d.mf2(); // Base::mf2()
	d.mf3(); // Derived::mf3()
	d.mf3(x); // Error
	return 0;
}

실행 결과

using 선언을 통해 앞서 발생했던 이름 가리기를 무시하여 정상적으로 컴파일, 실행된 결과를 볼 수 있다.

using을 public 영역에 한 이유는 어떤 파생 클래스가 기본 클래스로부터 public 상속으로 만들어진 것일 경우, 기본 클래스의 public 영역에 있는 이름들은 파생 클래스에서도 public 영역에 들어있어야 하기 때문이다.

 

private 상속과 간단한 전달 함수

기본 클래스가 가진 함수를 전부 상속하지 않기를 원하는 경우도 존재한다. 이 때 사용할 수 있는 것이 private 상속이다.

앞선 예제에서 Derived가 Base를 상속하는데 상속을 원하는 mf1 함수는 매개변수가 없는 버전만을 원한다고 가정하자. using 선언으로는 이름이 같은 모든 것들이 전부 파생 클래스에서 사용 가능해지기에 해결할 수 없다. 대신 간단한 전달 함수(forwarding function)를 이용할 수 있다. 

class Derived: private Base{
public:
	virtual void mf1(){ Base::mf1(); } // 간단한 전달 함수
	void mf3(){ cout << "Derived::mf3()" << endl; }
	void mf4(){ cout << "Derived::mf4()" << endl; }
};

int main(){
	Derived d;
	int x = 1234;
	d.mf1(); // OK!
	d.mf1(x); // Error
	return 0;
}

성공적으로 매개변수가 없는 mf1 함수만을 상속하여 사용할 수 있게 되었다.

매개변수가 있는 mf1 함수를 사용할 경우, 컴파일 에러를 발생시킨다.

이것만은 잊지 말자!

  • 파생 클래스의 이름은 기본 클래스의 이름을 가린다. public 상속에서는 이러한 이름 가림 현상을 바람직하지 않다.
  • 가려진 이름을 다시 볼 수 있게 하는 방법으로, using 선언 혹은 전달 함수를 쓸 수 있다.