program story

포인터 / 참조없이 다형성이 작동하지 않는 이유는 무엇입니까?

inputbox 2021. 1. 11. 08:07
반응형

포인터 / 참조없이 다형성이 작동하지 않는 이유는 무엇입니까?


나는 비슷한 제목으로 이미 SO에 대한 몇 가지 질문을 찾았지만 답변을 읽을 때 그들은 실제로 특정 질문 (예 : STL / 컨테이너)에 초점을 맞추고있었습니다.

누군가 다형성을 구현하기 위해 포인터 / 참조를 사용해야하는 이유를 보여줄 수 있습니까? 포인터가 도움이 될 수 있음을 이해할 수 있지만 참조는 값에 의한 전달과 참조에 의한 전달을 구분할 뿐입니 까 ??

확실히 메모리를 힙에 할당하여 동적 바인딩을 할 수 있다면 충분하지 않을 것입니다.


C ++에서 객체는 항상 컴파일 타임에 알려진 고정 된 유형과 크기를 가지며 (주소를 사용할 수있는 경우) 수명 기간 동안 항상 고정 주소에 존재합니다. 이는 C에서 상속 된 기능으로 두 언어 모두 저수준 시스템 프로그래밍에 적합합니다. (이 모든 것은 as-if, 규칙의 적용을받습니다. 준수 컴파일러는 보장되는 준수 프로그램의 동작에 감지 할 수있는 영향이 없음이 입증 될 수있는 한 코드로 원하는대로 자유롭게 수행 할 수 있습니다. 기준에 따라.)

virtualC ++ 함수는 객체의 런타임 유형을 기반으로 실행되는 것으로 정의됩니다 (어느 정도 극단적 인 언어 변호사가 필요 없음). 객체에서 직접 호출되면 항상 객체의 컴파일 타임 유형이되므로 virtual함수가 이런 방식으로 호출 될 때 다형성이 없습니다 .

반드시 그럴 필요는 없습니다. virtual함수가있는 객체 유형 은 일반적 virtual으로 각 유형에 고유 한 함수 테이블에 대한 객체 별 포인터를 사용하여 C ++로 구현됩니다 . 그래서 경 사진 경우, C ++의 일부 가상의 변형을위한 컴파일러 (예 : 객체에 할당 구현할 수있는 Base b; b = Derived()개체의 내용과 모두 복사 등) virtual그와 함께 테이블 포인터를하는 것 쉽게 일 경우 모두 BaseDerived같은 크기였습니다. 둘의 크기가 같지 않은 경우 컴파일러는 프로그램의 메모리를 재 배열하고 해당 메모리에 대한 가능한 모든 참조를 가능한 방식으로 업데이트하기 위해 임의의 시간 동안 프로그램을 일시 중지하는 코드를 삽입 할 수도 있습니다. 프로그램의 의미론에 감지 할 수있는 영향을 미치지 않는 것으로 입증되어 그러한 재배치가 발견되지 않으면 프로그램을 종료합니다. 그러나 이것은 매우 비효율적이며 중단되지 않을 수도 있습니다. 할당 연산자에게 바람직한 기능은 분명히 아닙니다. 있다.

따라서 위의 방법 대신 C ++의 다형성은 객체에 대한 참조 및 포인터가 선언 된 컴파일 타임 유형 및 그 하위 유형의 객체를 참조하고 가리 키도록 허용함으로써 수행됩니다. virtual함수가 참조 또는 포인터를 통해이라고하며, 그 특정 알려진 구현 런타임 타입이다에 컴파일러는 객체 참조 또는 지적 것을 증명할 수 virtual기능, 컴파일러 삽입 코드는 올바른 조회하는virtual런타임을 호출하는 함수. 참조와 포인터는 비다 형성 (선언 된 유형의 하위 유형을 참조하거나 가리키는 것을 허용하지 않음)으로 정의 될 수 있으며 프로그래머가 다형성을 구현하는 다른 방법을 제시하도록 강요 할 수 있습니다. . 후자는 C에서 항상 수행되기 때문에 분명히 가능하지만 그 시점에서 새로운 언어를 가질 이유가별로 없습니다.

요컨대, C ++의 의미 체계는 객체 지향 다형성의 높은 수준의 추상화 및 캡슐화를 허용하는 동시에 저수준 액세스 및 메모리의 명시 적 관리와 같은 기능을 유지하면서 다음과 같은 방식으로 설계되었습니다. 저수준 개발. 다른 의미론을 가진 언어를 쉽게 설계 할 수 있지만 C ++가 아니고 다른 장점과 단점이 있습니다.


"확실히 힙에 메모리를 할당하는 한"-메모리가 할당되는 위치는 그것과 아무 관련이 없습니다. 의미론에 관한 모든 것입니다. 예를 들어 :

Derived d;
Base* b = &d;

d스택 (자동 메모리)에 있지만 다형성은 b.

기본 클래스 포인터 나 파생 클래스에 대한 참조가 없으면 더 이상 파생 클래스가 없기 때문에 다형성이 작동하지 않습니다. 취하다

Base c = Derived();

c개체가 아닌 것입니다 Derived,하지만이 Base때문에, 슬라이스 . 따라서 기술적으로 다형성은 여전히 ​​작동합니다. 더 이상 Derived말할 대상 이 없다는 것입니다.

이제 가져가

Base* c = new Derived();

c메모리의 특정 위치를 가리키고 실제로 a Base또는 a 인지 여부는 신경 쓰지 Derived않지만 virtual메서드 호출 은 동적으로 해결됩니다.


다음과 같이 할당 할 때 복사 생성자가 호출된다는 것을 이해하면 정말 도움이됩니다.

class Base { };    
class Derived : public Base { };

Derived x; /* Derived type object created */ 
Base y = x; /* Copy is made (using Base's copy constructor), so y really is of type Base. Copy can cause "slicing" btw. */ 

y는 원래의 것이 아니라 Base 클래스의 실제 객체이므로 이것에 대해 호출되는 함수는 Base의 함수입니다.


리틀 엔디안 아키텍처를 고려하십시오. 값은 먼저 하위 바이트로 저장됩니다. 따라서 주어진 부호없는 정수에 대해 0-255 값은 값의 첫 번째 바이트에 저장됩니다. 모든 값의 하위 8 비트에 액세스하려면 해당 주소에 대한 포인터 만 있으면됩니다.

그래서 우리 uint8는 클래스로 구현할 수 있습니다. 의 인스턴스 uint8가 ... 1 바이트 라는 것을 알고 있습니다. 우리가하고 생산에서 파생 된 경우 uint16, uint32등의 인터페이스는 추상의 목적으로 동일하지만 하나의 가장 중요한 변화는 객체의 구체적인 인스턴스의 크기입니다.

우리가 구현하는 경우 물론, uint8char, 크기는 마찬가지로 동일 할 수있다 sint8.

그러나 operator=uint8uint16는 서로 다른 양의 데이터를 이동할 것입니다.

다형성 함수를 생성하려면 다음 중 하나를 수행 할 수 있어야합니다.

a / 데이터를 올바른 크기와 레이아웃의 새 위치에 복사하여 값으로 인수를받습니다. b / 객체 위치에 대한 포인터를 가져옵니다. c / 객체 인스턴스에 대한 참조를 가져옵니다.

템플릿을 사용하여 a를 달성 할 수 있으므로 다형성 포인터와 참조 없이도 작동 할 수 있지만 템플릿을 계산하지 않는 경우 구현 uint128하고 기대하는 함수에 전달 하면 어떻게되는지 고려해 보겠습니다 uint8. 답 : 128 대신 8 비트가 복사됩니다.

그래서 우리가 다형성 함수를 받아들이도록 uint128만들고 그것을 uint8. uint8불행히도 우리가 복사 하고있는 위치 있다면 , 우리 함수는 128 바이트를 복사하려고 시도 할 것입니다. 그 중 127은 접근 가능한 메모리 밖에 있습니다-> 충돌.

다음을 고려하세요:

class A { int x; };
A fn(A a)
{
    return a;
}

class B : public A {
    uint64_t a, b, c;
    B(int x_, uint64_t a_, uint64_t b_, uint64_t c_)
    : A(x_), a(a_), b(b_), c(c_) {}
};

B b1 { 10, 1, 2, 3 };
B b2 = fn(b1);
// b2.x == 10, but a, b and c?

fn컴파일 당시 에는에 대한 지식이 없었습니다 B. 그러나, B에서 파생 A우리가 호출 할 수 있도록해야하므로 다형성 fn로모그래퍼 B. 그러나 반환 하는 객체A 는 단일 int 구성 되어야합니다 .

B이 함수에 의 인스턴스를 전달하면 { int x; }a, b, c가없는 a 만 반환 됩니다.

이것은 "슬라이싱"입니다.

포인터와 참조로도 우리는 이것을 무료로 피하지 않습니다. 중히 여기다:

std::vector<A*> vec;

Elements of this vector could be pointers to A or something derived from A. The language generally solves this through the use of the "vtable", a small addition to the object's instance which identifies the type and provides function pointers for virtual functions. You can think of it as something like:

template<class T>
struct PolymorphicObject {
    T::vtable* __vtptr;
    T __instance;
};

Rather than every object having its own distinct vtable, classes have them, and object instances merely point to the relevant vtable.

The problem now is not slicing but type correctness:

struct A { virtual const char* fn() { return "A"; } };
struct B : public A { virtual const char* fn() { return "B"; } };

#include <iostream>
#include <cstring>

int main()
{
    A* a = new A();
    B* b = new B();
    memcpy(a, b, sizeof(A));
    std::cout << "sizeof A = " << sizeof(A)
        << " a->fn(): " << a->fn() << '\n';
}          

http://ideone.com/G62Cn0

sizeof A = 4 a->fn(): B

What we should have done is use a->operator=(b)

http://ideone.com/Vym3Lp

but again, this is copying an A to an A and so slicing would occur:

struct A { int i; A(int i_) : i(i_) {} virtual const char* fn() { return "A"; } };
struct B : public A {
    int j;
    B(int i_) : A(i_), j(i_ + 10) {}
    virtual const char* fn() { return "B"; }
};

#include <iostream>
#include <cstring>

int main()
{
    A* a = new A(1);
    B* b = new B(2);
    *a = *b; // aka a->operator=(static_cast<A*>(*b));
    std::cout << "sizeof A = " << sizeof(A)
        << ", a->i = " << a->i << ", a->fn(): " << a->fn() << '\n';
}       

http://ideone.com/DHGwun

(i is copied, but B's j is lost)

The conclusion here is that pointers/references are required because the original instance carries membership information with it that copying may interact with.

But also, that polymorphism is not perfectly solved within C++ and one must be cognizant of their obligation to provide/block actions which could produce slicing.


You need pointers or reference because for the kind of polymorphism you are interested in (*), you need that the dynamic type could be different from the static type, in other words that the true type of the object is different than the declared type. In C++ that happens only with pointers or references.


(*) Genericity, the type of polymorphism provided by templates, doesn't need pointers nor references.


When an object is passed by value, it's typically put on the stack. Putting something on the stack requires knowledge of just how big it is. When using polymorphism, you know that the incoming object implements a particular set of features, but you usually have no idea the size of the object (nor should you, necessarily, that's part of the benefit). Thus, you can't put it on the stack. You do, however, always know the size of a pointer.

Now, not everything goes on the stack, and there are other extenuating circumstances. In the case of virtual methods, the pointer to the object is also a pointer to the object's vtable(s), which indicate where the methods are. This allows the compiler to find and call the functions, regardless of what object it's working with.

Another cause is that very often the object is implemented outside of the calling library, and allocated with a completely different (and possibly incompatible) memory manager. It could also have members that can't be copied, or would cause problems if they were copied with a different manager. There could be side-effects to copying and all sorts of other complications.

The result is that the pointer is the only bit of information on the object that you really properly understand, and provides enough information to figure out where the other bits you need are.

ReferenceURL : https://stackoverflow.com/questions/15188894/why-doesnt-polymorphism-work-without-pointers-references

반응형