Stroustrup은 "모든 클래스 (객체 클래스)에 대한 고유 한 기반을 즉시 발명하지 마십시오. 일반적으로 대부분 / 대부분의 클래스를 사용하지 않고도 더 잘 수행 할 수 있습니다." (C ++ 프로그래밍 언어 제 4 판, 1.3.4 절)
모든 것을위한 기본 클래스가 일반적으로 나쁜 생각 인 이유는 무엇이며 언제 만드는 것이 합리적입니까?
Stroustrup은 "모든 클래스 (객체 클래스)에 대한 고유 한 기반을 즉시 발명하지 마십시오. 일반적으로 대부분 / 대부분의 클래스를 사용하지 않고도 더 잘 수행 할 수 있습니다." (C ++ 프로그래밍 언어 제 4 판, 1.3.4 절)
모든 것을위한 기본 클래스가 일반적으로 나쁜 생각 인 이유는 무엇이며 언제 만드는 것이 합리적입니까?
답변:
그 객체가 기능을 위해 무엇을 가지고 있을까요? 자바에서 모든 Base 클래스에는 toString, hashCode & equality 및 monitor + condition 변수가 있습니다.
ToString은 디버깅에만 유용합니다.
hashCode는 해시 기반 컬렉션에 저장하려는 경우에만 유용합니다 (C ++의 기본 설정은 해시 함수를 템플릿 매개 변수로 컨테이너에 전달하거나 std::unordered_*
완전히 피하고 std::vector
순서가없는 일반 목록을 사용하는 것입니다).
기본 객체가없는 평등은 컴파일 타임에 도움이 될 수 있습니다. 같은 유형이 없으면 같을 수 없습니다. C ++에서 이것은 컴파일 시간 오류입니다.
모니터 및 조건 변수는 경우에 따라 명시 적으로 포함하는 것이 좋습니다.
그러나 필요한 것이 더 있으면 유스 케이스가 있습니다.
예를 들어 QT에는 QObject
스레드 선호도, 부모-자식 소유권 계층 및 신호 슬롯 메커니즘의 기반을 형성 하는 루트 클래스가 있습니다. 또한 QObject에 대한 포인터로 강제 사용하지만 Qt의 많은 클래스 는 신호 슬롯 (특히 설명의 값 유형)이 필요하지 않기 때문에 QObject를 상속하지 않습니다 .
Object
.
_hashCode
아니다'다른 컨테이너를 사용 '이 아니라 가리키는 C ++ std::unordered_map
은 구현을 제공하기 위해 요소 클래스 자체를 요구하지 않고 템플릿 인수를 사용하여 해싱을 수행합니다. 즉, C ++의 다른 모든 좋은 컨테이너 및 리소스 관리자와 마찬가지로 방해가되지 않습니다. 누군가가 나중에 어떤 맥락에서 필요할 때를 대비 하여 함수 또는 데이터로 모든 객체를 오염시키지 않습니다 .
모든 객체가 공유하는 기능이 없기 때문입니다. 이 클래스에는 모든 클래스에 적합한 인터페이스가 없습니다.
키가 큰 상속 계층 구조를 만들 때마다 Fragile Base Class (Wikipedia.) 의 문제가 발생하는 경향이 있습니다 .
작은 별개의 (상이하고 분리 된) 상속 계층 구조가 많으면이 문제가 발생할 가능성이 줄어 듭니다.
모든 객체를 하나의 거대한 상속 계층 구조의 일부로 만들면 실제로이 문제가 발생할 수 있습니다.
cout.print(x).print(0.5).print("Bye\n")
– 그것은 힌지가 아닙니다 operator<<
.
때문에:
모든 종류의 virtual
기능을 구현 하면 가상 테이블이 생겨나 고 많은 (대부분의) 상황에서 필요하지 않은 오브젝트 별 공간 오버 헤드가 필요합니다.
비 toString
가상적으로 구현 하는 것은 반환 할 수있는 유일한 것은 객체 주소입니다.이 주소는 Java와 달리 사용자에게 매우 친숙하지 않으며 호출자가 이미 액세스 할 수 있기 때문입니다.
마찬가지로 비 가상적 equals
이거나 hashCode
주소 만 사용하여 객체를 비교할 수 있습니다. 이는 Java와 달리 C ++에서 객체가 자주 복사되므로 객체의 "정체성"을 구분하는 것조차별로 중요하지 않습니다. 항상 의미 있거나 유용합니다. (예를 들어 int
실제로는 값 이외의 정체성을 가져서는 안됩니다 ... 같은 값의 두 정수는 같아야합니다.)
open
shared_ptr<Foo>
그것은 또한 있는지 확인하기 위해 shared_ptr<Bar>
(다른 포인터 타입 또는 마찬가지로) 경우에도, Foo
그리고 Bar
서로에 대해 아무것도 몰라 관련이없는 클래스이다. 그러한 것들이 어떻게 사용되는지에 대한 역사를 고려할 때 "원시 포인터"와 함께 작동하는 것은 비용이 많이 들지만, 어쨌든 힙 저장 될 것이라면 추가 비용은 최소화 될 것입니다.
하나의 루트 객체를 가지면 많은 보상없이 수행 할 수있는 작업과 컴파일러가 수행 할 수있는 작업이 제한됩니다.
공통 루트 클래스를 사용하면 모든 컨테이너를 작성하고로 포함 된 것을 추출 할 수 dynamic_cast
있지만 컨테이너가 필요한 경우 공통 루트 클래스 없이도 비슷한 것을 boost::any
수행 할 수 있습니다 . 그리고 또한 프리미티브를 지원합니다 - 심지어 작은 버퍼 최적화를 지원하고 자바 용어로 거의 "박스 없음"을 남길 수 있습니다.boost::any
C ++는 가치 유형을 지원하고 번성합니다. 리터럴과 프로그래머가 작성한 값 유형. C ++ 컨테이너는 값 유형을 효율적으로 저장, 정렬, 해시, 소비 및 생성합니다.
상속, 특히 모 놀리 식 상속 Java 스타일 기본 클래스는 프리 스토어 기반의 "포인터"또는 "참조"유형을 필요로합니다. 핸들 / 포인터 / 데이터 참조는 클래스의 인터페이스에 대한 포인터를 보유하며 다형성으로 다른 것을 나타낼 수 있습니다.
일부 상황에서는 유용하지만 "공통 기본 클래스"를 사용하여 패턴과 결혼 한 후에는 유용하지 않은 경우에도 전체 패턴베이스를이 패턴의 비용과 수하물에 고정했습니다.
호출 사이트 나 코드를 사용하는 코드에서 "객체"라는 것보다 유형에 대해 더 많이 알고있을 것입니다.
함수가 단순하면 함수를 템플리트로 작성하면 호출 사이트의 정보가 버려지지 않는 오리 유형 컴파일 시간 기반 다형성이 제공됩니다. 기능이 더 복잡한 경우 수행하려는 유형 (예 : 직렬화 및 역 직렬화)에 대한 균일 한 작업을 빌드하고 저장 (컴파일 시간에)하여 런타임에 소비 할 수 있도록 유형 삭제를 수행 할 수 있습니다. 다른 번역 단위의 코드.
모든 것을 직렬화 할 수있는 라이브러리가 있다고 가정하십시오. 한 가지 방법은 기본 클래스를 사용하는 것입니다.
struct serialization_friendly {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~serialization_friendly() {}
};
이제 여러분이 작성하는 모든 코드가 가능합니다 serialization_friendly
.
void serialize( my_buffer* b, serialization_friendly const* x ) {
if (x) x->write_to(b);
}
를 제외한 std::vector
모든 컨테이너를 작성해야합니다. 그리고 그 큰 숫자 라이브러리에서 얻은 정수는 아닙니다. 그리고 그런 유형은 직렬화가 필요하지 않다고 썼습니다. 그리고이 아니 tuple
거나 int
또는 double
, 나 std::ptrdiff_t
.
우리는 다른 접근 방식을 취합니다.
void write_to( my_buffer* b, int x ) {
b->write_integer(x);
}
template<class T,
class=std::enable_if_t< void_t<
std::declval<T const*>()->write_to( std::declval<my_buffer*>()
> >
>
void write_to( my_buffer* b, T const* x ) {
if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
write_to( b, t );
}
겉보기에는 아무것도하지 않는 것으로 구성되어 있습니다. 이제는 유형의 네임 스페이스 또는 유형의 메서드에서 자유 함수로 write_to
재정 의하여 확장 할 수 있습니다 write_to
.
우리는 약간의 삭제 코드를 작성할 수도 있습니다.
namespace details {
struct can_serialize_pimpl {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~can_serialize_pimpl() {}
};
}
struct can_serialize {
void write_to( my_buffer* b ) const { pImpl->write_to(b); }
void read_from( my_buffer const* b ) { pImpl->read_from(b); }
std::unique_ptr<details::can_serialize_pimpl> pImpl;
template<class T> can_serialize(T&&);
};
namespace details {
template<class T>
struct can_serialize : can_serialize_pimpl {
std::decay_t<T>* t;
void write_to( my_buffer*b ) const final override {
serialize( b, std::forward<T>(*t) );
}
void read_from( my_buffer const* ) final override {
deserialize( b, std::forward<T>(*t) );
}
can_serialize(T&& in):t(&in) {}
};
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}
이제 임의의 유형을 가져 와서 가상 인터페이스를 통해 나중에 can_serialize
호출 할 수 있는 인터페이스에 자동 상자를 넣을 수 있습니다 serialize
.
그래서:
void writer_thingy( can_serialize s );
대신 직렬화 할 수있는 모든 것을 취하는 함수입니다.
void writer_thingy( serialization_friendly const* s );
그리고 첫 번째, 두 번째는 달리, 그것은 처리 할 수있는 int
, std::vector<std::vector<Bob>>
자동으로.
작성하는 데 많은 시간이 걸리지 않았으며, 특히 이런 종류의 일은 당신이 거의 원하지 않는 일이기 때문에 기본 유형을 요구하지 않고 직렬화 가능한 것으로 취급 할 수있는 능력을 얻었습니다.
무엇을 더, 지금 우리가 할 수 std::vector<T>
단순히 대체하여 일류 시민으로 직렬화 write_to( my_buffer*, std::vector<T> const& )
- 그 과부하, 그것은에 전달 될 수 can_serialize
와의 serializabilty는 std::vector
의 vtable에 저장하고 액세스됩니다 .write_to
.
간단히 말해 C ++은 필요할 때 강제 상속 계층 구조의 가격을 지불하지 않고도 필요할 때 즉시 단일 기본 클래스의 이점을 구현할 수있을 정도로 강력합니다. 그리고 단일 염기 (가짜 또는 불필요)가 필요한 시간은 합리적입니다.
유형이 실제로 자신의 정체성이고 그 유형을 알면 최적화 기회가 많이 있습니다. 데이터는 로컬에 연속적으로 저장되어 있으며 (현대 프로세서의 캐시 친화성에 매우 중요), 컴파일러는 불투명 한 가상 메소드 포인터를 사용하지 않고 지정된 작업이 수행하는 작업을 쉽게 이해할 수 있습니다. 다른 쪽)을 사용하면 명령을 최적으로 재정렬하고 둥근 구멍에 적은 수의 둥근 못을 박을 수 있습니다.
위의 많은 좋은 답변이 있으며 @ratchetfreak의 답변에 표시된 것처럼 다른 방법으로 기본 객체로 수행하는 모든 작업을 더 잘 수행 할 수 있다는 명확한 사실이 있지만 그에 대한 의견은 매우 중요하지만 상속 다이아몬드를 만들지 않는 또 다른 이유가 있습니다.다중 상속이 사용될 때. 범용 기본 클래스에서 기능이있는 경우 다중 상속 사용을 시작하자마자 액세스 체인의 변형에 따라 상속 체인의 경로에 따라 다르게 오버로드 될 수있는 변형을 지정해야합니다. 또한 비효율적 일 수 있기 때문에베이스는 가상이 될 수 없습니다 (메모리 사용 및 로컬 리티에서 잠재적으로 막대한 비용으로 모든 오브젝트에 가상 테이블이 있어야 함). 이것은 매우 빠른 물류 악몽이 될 것입니다.
사실 Microsoft의 초기 C ++ 컴파일러 및 라이브러리 (Visual C ++, 16 비트에 대해 알고 있음)에는 이와 같은 클래스가 CObject
있습니다.
그러나 그 당시 "templates"는이 간단한 C ++ 컴파일러에 의해 지원되지 않았으므로 이와 같은 클래스 std::vector<class T>
는 불가능했습니다. 대신 "벡터"구현은 한 가지 유형의 클래스 만 처리 할 수 있으므로 std::vector<CObject>
오늘날 과 비슷한 클래스가있었습니다 . CObject
거의 모든 클래스의 기본 클래스 이기 때문에 (불행히도 현대 컴파일러 CString
와 동일 하지는 않음 string
) 거의 모든 종류의 객체를 저장하는 데이 클래스를 사용할 수 있습니다.
최신 컴파일러는 템플릿을 지원하기 때문에 "일반적인 기본 클래스"사용 사례는 더 이상 제공되지 않습니다.
이러한 일반 기본 클래스를 사용하면 생성자 호출과 같은 메모리와 런타임에 약간의 비용이 소요된다는 사실에 대해 생각해야합니다. 따라서 이러한 클래스를 사용할 때는 단점이 있지만 최소한 현대적인 C ++ 컴파일러를 사용할 때는 이러한 클래스에 대한 사용 사례가 거의 없습니다.
TObject
MFC가 존재하기 전에 자체적으로 가지고 있었습니다. 디자인의 그 부분에 대해 Microsoft를 비난하지 마십시오. 당시의 거의 모든 사람들에게 좋은 생각처럼 보였습니다.
Java에서 나온 또 다른 이유를 제안하려고합니다.
때문에 당신 에 대한 기본 클래스 만들 수 없습니다 모든 것을 적어도 보일러 플레이트의 무리없이.
당신은 당신의 자신의 클래스를 위해 그것을 벗어날 수 있습니다-그러나 아마도 당신은 많은 코드를 복제하게 될 것입니다. 예 : " std::vector
구현되지 않기 때문에 여기서 사용할 수 없습니다 . 올바른 일을 IObject
하는 새로운 파생물 IVectorObject
을 만드는 것이 좋습니다 ."
이것은 내장 라이브러리 나 표준 라이브러리 클래스 또는 다른 라이브러리의 클래스를 다룰 때마다 해당됩니다.
이 언어에 내장 된 이제 경우는 같은 것들로 끝날 것 Integer
와 int
자바에 혼란, 또는 언어 구문에 큰 변화. (내가 생각하기에 다른 언어는 모든 유형으로 작성하는 데 훌륭한 일을했다고 생각합니다. 루비가 더 좋은 예처럼 보입니다.)
또한 기본 클래스가 런타임 다형성이 아닌 경우 (예 : 가상 함수 사용) 프레임 워크와 같은 특성을 사용하면 동일한 이점을 얻을 수 있습니다.
예를 들어 다음 대신에 .toString()
다음을 수행 할 수 있습니다.
template<typename T>
struct ToStringTrait;
template<typename T>
std::string toString(const T & t) {
return ToStringTrait<T>::toString(t);
}
template<>
struct ToStringTrait<int> {
std::string toString(int v) {
return itoa(v);
}
}
template<typename T>
struct ToStringTrait<std::vector<T>> {
std::string toString(const std::vector<T> &v) {
std::stringstream ss;
ss<<"{";
for(int i=0; i<v.size(); ++i) {
ss<<toString(v[i]);
}
ss<<"}";
return ss.str();
}
}
틀림없이 "공허"는 보편적 기본 계층의 많은 역할을 수행합니다. 당신은 포인터를void*
. 그런 다음 해당 포인터를 비교할 수 있습니다. static_cast
원래 수업으로 돌아갈 수 있습니다 .
그러나 당신이 무엇을 할 수 와 함께 할 수 void
있는 당신이 할 수있는 Object
당신이 정말로이 객체의 유형을 파악하기 위해 사용 RTTI입니다. 이것은 궁극적으로 C ++의 모든 객체가 RTTI를 갖지 않는 방법에 따라 달라지며 실제로 너비가 0 인 객체를 가질 수 있습니다.
[[no_unique_address]]
를 통해 멤버 하위 객체에 너비를 0으로 지정하기 위해 컴파일러에서 사용할 수있는을 추가합니다 .
[[no_unique_address]]
컴파일러가 EBO 멤버 변수를 허용합니다.
Java는 정의되지 않은 동작이 없어야 한다는 디자인 철학을 채택 합니다 . 다음과 같은 코드 :
Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();
인터페이스를 구현 felix
하는 하위 유형을 보유 하는지 테스트합니다 . 그렇지 않으면 캐스트를 수행하고 호출 하지 않으면 예외가 발생합니다. 코드의 동작은 완전히 여부를 정의 구현 여부 .Cat
Woofer
woof()
felix
Woofer
C ++은 프로그램이 어떤 조작을 시도하지 않아야한다면, 그 조작이 시도 된 경우 생성 된 코드가 무엇을하든 상관 없으며, 컴퓨터는 "해야 할"행동을 제한하려고 시도하는 데 시간을 낭비해서는 안된다는 철학을 취합니다. 절대 발생하지 않습니다. C ++에서 a에 캐스트하기 *Cat
위해 적절한 간접 연산자를 추가하면 *Woofer
코드는 캐스트가 합법적 인 경우 정의 된 동작을 생성 하지만 그렇지 않은 경우 정의되지 않은 동작을 생성합니다 .
사물에 대한 공통 기본 유형을 사용하면 해당 기본 유형의 파생 상품 간의 캐스트 유효성을 검사하고 캐스트 캐스트 작업을 수행 할 수 있지만 캐스트 유효성 검사는 합법적이고 나쁜 일이 발생하지 않는다고 가정하는 것보다 비용이 많이 듭니다. C ++ 철학은 그러한 유효성 검사에는 "일반적으로 필요하지 않은 것에 대한 비용 지불"이 필요하다는 것입니다.
C ++와 관련되어 있지만 새로운 언어에는 문제가되지 않는 또 다른 문제는 여러 프로그래머가 각각 공통 기반을 만들면 해당 클래스를 파생하고 해당 공통 기본 클래스의 작업을 수행하는 코드를 작성한다는 것입니다. 이러한 코드는 다른 기본 클래스를 사용한 프로그래머가 개발 한 객체로는 작동하지 않습니다. 새로운 언어가 모든 힙 객체에 공통 헤더 형식이 필요하고 힙 객체가 허용하지 않은 힙 객체를 허용하지 않은 경우 이러한 헤더가있는 힙 객체에 대한 참조가 필요한 메소드는 모든 힙 객체에 대한 참조를 승인합니다. 이제까지 만들 수 있습니다.
개인적으로, 객체를 "X 형으로 변환 할 수 있는가?"라는 일반적인 방법을 사용하는 것이 언어 / 프레임 워크에서 매우 중요한 기능이라고 생각합니다. 그러나 그러한 기능이 처음부터 언어에 내장되어 있지 않다면 어렵습니다. 나중에 추가하십시오. 개인적으로, 나는 그러한 기본 클래스를 처음에 표준 라이브러리에 추가해야한다고 생각합니다. 다형성으로 사용될 모든 객체는 해당 기본 클래스에서 상속해야합니다. 프로그래머가 각각 고유 한 "기본 유형"을 구현하면 서로 다른 사람들의 코드간에 객체를 전달하는 것이 더 어려워 지지만 많은 프로그래머가 상속 한 공통 기본 유형을 사용하면 더 쉬워집니다.
추가
템플릿을 사용하여 "임의의 객체 홀더"를 정의하고 그 안에 포함 된 객체의 유형에 대해 물어볼 수 있습니다. Boost 패키지에는라는 것이 포함되어 있습니다 any
. 따라서 C ++에 표준 "유형 검사 가능 참조"유형이 없더라도 작성할 수 있습니다. 이것은 언어 표준에 무언가가 없다는 것, 즉 다른 프로그래머의 구현 사이의 비 호환성으로 인해 발생하는 문제를 해결하지 못하지만 모든 것이 파생되는 기본 유형을 갖지 않고 C ++가 얻는 방법을 설명합니다. 하나처럼 행동하는 것.
Woofer
인터페이스이고 Cat
상속 가능한 경우 캐스트에서 WoofingCat
상속 Cat
하고 구현할 가능성이 있기 때문에 캐스트는 합법적 Woofer
입니다. Java 컴파일 / 링크 모델에서 a를 WoofingCat
만들려면 Cat
nor 에 대한 소스 코드에 액세스 할 필요가 없습니다 Woofer
.
Cat
로의 캐스트 시도를 올바르게 처리하고 Woofer
"X 유형으로 변환 할 수 있습니까?"라는 질문에 대답합니다. C ++을 사용하면 캐스트를 강요 할 수 있습니다. 원인을 알 수 있습니다. 실제로 무엇을하고 있는지 알 수 있지만 실제로 의도 한 것이 아닌 경우 도움이됩니다.
dynamic_cast
, 다형성 객체를 가리키는 경우 포인터를 정의하면 동작이 정의되고 그렇지 않은 경우 정의되지 않은 동작은 의미 적 관점에서 볼 수 있습니다.