소멸자가 두 번 처형 된 이유는 무엇입니까?


12
#include <iostream>
using namespace std;

class Car
{
public:
    ~Car()  { cout << "Car is destructed." << endl; }
};

class Taxi :public Car
{
public:
    ~Taxi() {cout << "Taxi is destructed." << endl; }
};

void test(Car c) {}

int main()
{
    Taxi taxi;
    test(taxi);
    return 0;
}

이것은 출력입니다 :

Car is destructed.
Car is destructed.
Taxi is destructed.
Car is destructed.

MS Visual Studio Community 2017을 사용하고 있습니다 (죄송합니다. Visual C ++ 버전을 보는 방법을 모르겠습니다). 디버그 모드를 사용했을 때. void test(Car c){ }함수 본문을 예상대로 떠날 때 하나의 소멸자가 실행된다는 것을 알았습니다 . 그리고 끝날 때 여분의 소멸자가 나타났습니다 test(taxi);.

test(Car c)함수는 값을 공식 매개 변수로 사용합니다. 기능에 갈 때 자동차가 복사됩니다. 그래서 나는 기능을 떠날 때 단 하나의 "자동차가 파괴된다"고 생각했다. 그러나 실제로 함수를 떠날 때 두 개의 "Car is destructed"가 있습니다. 감사합니다.

===============

class Car 예를 들어 가상 함수를 추가 virtual void drive() {} 하면 예상 출력이 나타납니다.

Car is destructed.
Taxi is destructed.
Car is destructed.

3
값 으로 객체를 가져 오는 함수에 객체를 전달할 때 컴파일러가 객체 슬라이싱을 처리하는 방법에 문제가있을 수 있습니까? TaxiCar
일부 프로그래머 친구

1
이전 C ++ 컴파일러 여야합니다. g ++ 9는 예상 결과를 제공합니다. 디버거를 사용하여 오브젝트의 추가 사본이 작성되는 이유를 판별하십시오.
Sam Varshavchik

2
버전 7.4.0의 g ++ 및 버전 6.0.0의 clang ++를 테스트했습니다. 그들은 op의 출력과 다른 예상 출력을 주었다. 따라서 문제는 그가 사용하는 컴파일러에 관한 것일 수 있습니다.
Marceline

1
MS Visual C ++로 재현했습니다. 사용자 정의 복사 생성자와 기본 생성자를 추가 Car하면이 문제가 사라지고 예상 결과가 나타납니다.
interjay

1
질문에 컴파일러와 버전을 추가하십시오
Lightness Races in Orbit

답변:


7

Visual Studio 컴파일러가 taxi함수 호출을 슬라이싱 할 때 약간의 지름길을 취하는 것처럼 보이 므로 아이러니하게도 예상보다 많은 작업을 수행합니다.

먼저, 인수가 일치하도록 taxi복사 및 생성합니다 Car.

그런 다음 값으로 전달하기 위해 Car 다시 복사합니다 .

이 동작은 사용자 정의 복사 생성자를 추가하면 사라 지므로 컴파일러는 자체적으로 (허용하면 내부적으로 더 간단한 코드 경로 임)이를 수행하는 것처럼 보입니다. 사본 자체는 사소합니다. 사소한 소멸자를 사용하여이 동작을 계속 관찰 할 수 있다는 사실은 약간의 수차입니다.

이것이 합법적 인 정도 (특히 C ++ 17 이후) 또는 컴파일러 가이 접근법을 취하는 이유를 모르겠지만 직관적으로 기대했던 결과가 아니라는 데 동의합니다. GCC 나 Clang은 같은 방식으로 작업을 수행 할 수 있지만 복사본을 더 잘 구사할 수는 있지만 그렇게하지는 않습니다. 나는 도 VS 2019는 여전히 보장 생략에없는 대단한 것으로 나타났습니다.


죄송합니다. "컴파일러가 복사 제거를 수행하지 않는 경우 택시에서 자동차로의 변환"이라고 말한 것과 정확히 일치하지 않습니다.
Christophe

슬라이싱을 피하기위한 참조 별 전달 대 참조에 의한 통과는이 질문 이외의 OP를 돕기 위해 편집에만 추가 되었기 때문에 이는 부당한 설명입니다. 그런 다음 내 대답은 어둠 속에서 촬영되지 않았으며 처음부터 명확하게 설명되었으며 동일한 결론에 도달하게되어 기쁩니다. 이제 여러분의 공식을 살펴보면, "모른 것 같습니다 ... 모르겠습니다"라고 생각합니다. 왜냐하면 컴파일러가 왜이 temp를 생성해야하는지 이해하지 못하기 때문입니다.
Christophe

좋아, 대답의 관련없는 부분을 제거하면 관련 단락 하나만 남습니다
Orbit in Orbit

좋아, 산만 슬라이싱 파라를 제거하고 표준에 대한 정확한 참조로 복사 제거에 대한 요점을 정당화했습니다.
Christophe

택시에서 임시 자동차를 복사 한 다음 다시 매개 변수에 복사해야하는 이유를 설명해 주시겠습니까? 그리고 왜 일반 자동차가 제공 될 때 컴파일러가 이것을하지 않는가?
Christophe

3

무슨 일이야?

당신이를 만들 때 Taxi, 당신은 또한 만들 Car하위 객체를. 그리고 택시가 파괴되면 두 물체가 모두 파괴됩니다. 전화 test()하면 Carby 값 을 전달합니다 . 따라서 두 번째 Car사본은 복사 구성되고 test()남겨두면 소멸됩니다 . 그래서 우리는 3 개의 소멸자에 대해 설명했습니다 : 첫 번째와 두 개의 마지막 순서.

네 번째 소멸자 (즉, 시퀀스에서 두 번째)는 예상치 못한 것이며 다른 컴파일러로는 재현 할 수 없었습니다.

인수의 Car소스로 임시로만 작성할 수 있습니다 Car. Car인수로 직접 값을 제공 할 때 발생하지 않으므로 로 변환하는 것으로 생각 Taxi됩니다 Car. Car모든에 이미 하위 객체 가 있기 때문에 이것은 예기치 않은 일 Taxi입니다. 따라서 컴파일러는 임시로 불필요한 변환을 수행 하고이 임시를 피할 수있는 복사 제거를하지 않는다고 생각합니다.

의견에 대한 설명 :

다음은 언어 변호사가 내 주장을 확인하기위한 표준을 참조한 설명입니다.

  • 여기서 언급 한 변환은 constructor [class.conv.ctor]에 의한 변환입니다 . 즉 다른 유형 (여기서는 택시)의 인수를 기반으로 한 클래스 (여기서는 Car)의 객체를 생성합니다.
  • 이 변환은 Car값 을 반환하기 위해 임시 개체를 사용 합니다. 컴파일러는 [class.copy.elision]/1.1임시를 구성하는 대신 값을 매개 변수로 직접 리턴하도록 구성 할 수 있으므로 에 따라 복사 제거를 수행 할 수 있습니다.
  • 따라서이 온도가 부작용을 일으키는 경우, 컴파일러는이 가능한 복사 제거를 사용하지 않기 때문입니다. 복사 제거가 필수가 아니기 때문에 잘못된 것은 아닙니다.

분석의 실험적 확인

이제 동일한 컴파일러를 사용하여 사례를 재현하고 실험을 진행하여 진행 상황을 확인할 수 있습니다.

위의 가정은 컴파일러 Car(const &Taxi)가의 Car하위 객체 에서 직접 복사를 생성하는 대신 생성자 변환을 사용하여 차선의 매개 변수 전달 프로세스를 선택했다고 가정했습니다 Taxi.

그래서 호출 test()했지만 명시 적으로에 캐스팅 Taxi했습니다 Car.

나의 첫 번째 시도는 상황을 개선하는 데 성공하지 못했습니다. 컴파일러는 여전히 차선의 생성자 변환을 사용했습니다.

test(static_cast<Car>(taxi));  // produces the same result with 4 destructor messages

나의 두 번째 시도는 성공했다. 캐스팅도 수행하지만 컴파일러 가이 어리석은 임시 개체를 만들지 않고 Car하위 개체 를 사용하도록 강력히 제안하기 위해 포인터 캐스팅을 사용 Taxi합니다.

test(*static_cast<Car*>(&taxi));  //  :-)

그리고 놀랍습니다 : 예상대로 작동하여 3 개의 파괴 메시지 만 생성합니다 :-)

결론적 인 실험 :

마지막 실험에서 변환을 통해 사용자 정의 생성자를 제공했습니다.

 class Car {
 ... 
     Car(const Taxi& t);  // not necessary but for experimental purpose
 }; 

로 구현하십시오 *this = *static_cast<Car*>(&taxi);. 어리석게 들리지만 3 개의 소멸자 메시지 만 표시하는 코드를 생성하므로 불필요한 임시 객체를 피할 수 있습니다.

이것은 컴파일러에이 동작을 일으키는 버그가있을 수 있다고 생각하게합니다. 상황에 따라 기본 클래스에서 직접 복사 구성을 놓칠 가능성이 있습니다.


2
질문에 대답하지 않습니다
가벼움 궤도에서 궤도

1
@qiazi 필자는 이것이 호출 호출 컨텍스트에서 함수 외부에서 생성되므로 복사 제거없이 변환에 대한 임시 가설을 확인한다고 생각합니다.
Christophe

1
"컴파일러가 복사 제거를 수행하지 않으면 택시에서 자동차로 변환"이라고 말할 때 어떤 복사 제거를 의미합니까? 처음에는 생략해야 할 사본이 없어야합니다.
interjay

1
컴파일러는 변환을 수행하기 위해 Taxi의 Car 하위 오브젝트를 기반으로 Car 임시를 구성한 다음이 임시를 Car 매개 변수에 복사 할 필요가 없기 때문에 @interjay : 사본을 제거하고 원래 하위 오브젝트에서 매개 변수를 직접 구성 할 수 있습니다.
Christophe

1
사본 제거는 표준에서 사본을 작성해야한다고 명시하지만 특정 상황에서는 사본을 제거 할 수 있습니다. 이 경우 처음부터 사본을 작성해야하는 이유가 없으므로 ( 복사 생성자에 Taxi직접 전달할 수 있는 참조 Car) 사본 제거는 관련이 없습니다.
interjay
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.