작성
·
193
2
지금까지 들은 강의에서는 매개변수에 &
가 있는 경우에는 call by reference로 동작하고, copy를 하지 않는 방식으로 알고 있었습니다. 그리고 call by value로 동작하는 경우에는 상황에 따라서 object slicing이 발생할 가능성이 있다고 이해했었습니다.
그런데 강의에서 아래 코드처럼 &
이 있는데 object slicing이 발생하신다고 하셔서 몹시 혼란스럽더라구요.
catch(Exception& e) {
e.report();
}
그래서 다른 분들의 질답글과 C++ 표준(n3690을 참고했습니다)을 찾아봤습니다.
Throwing an exception copy-initializes (8.5, 12.8) a temporary object, called the exception object. The temporary is an lvalue and is used to initialize the variable named in the matching handler (15.3). If the type of the exception object would be an incomplete type or a pointer to an incomplete type other than (possibly cv-qualified) void the program is ill-formed. Evaluating a throw-expression with an operand throws an exception; the type of the exception object is determined by removing any top-level cv-qualifiers from the static type of the operand and adjusting the type from “array of T” or “function returning T” to “pointer to T” or “pointer to function returning T,” respectively
- C++ ISO n3690 15.1/3
내용을 보니 throw
를 하게 되면 exception object(예외 객체)라는 것을 copy-initialize하고, 해당 객체는 l-value이고 일치하는 handler에서 변수를 초기화하기 위해 사용된다고 되어 있었습니다.
그리고 영상에서 rethrow 시 throw;
처럼 피연산자가 없는 경우 object slicing이 발생하지 않는다고만 나와서 설명이 좀 빈약한 것 같아 더 찾아봤습니다.
A throw-expression with no operand rethrows the currently handled exception (15.3). The exception is reactivated with the existing exception object; no new exception object is created. The exception is no longer considered to be caught; therefore, the value of std::uncaught_exception() will again be true.
- C++ ISO n3690 15.1/8
throw;
의 경우 새로운 exception object를 생성하지 않고, 기존 exception object로 다시 예외를 활성화시킨다고 나와있었습니다.
좀 정확하게 확인하려고 예외 객체의 주소를 각 시점에 출력하도록 했습니다.
class MyArray {
private:
int m_data[5];
public:
int& operator [](const int& index) {
if (index < 0 || 5 <= index) {
ArrayException e;
cout << &e << endl;
throw e;
}
return m_data[index];
}
};
void doSomething() {
MyArray my_array;
try {
my_array[100];
}
catch (Exception& e) {
cout << "doSomething()" << endl;
cout << &e << endl;
e.report();
throw e;
}
}
int main() {
try {
doSomething();
}
catch (Exception& e) {
cout << "main()" << endl;
cout << &e << endl;
e.report();
}
return 0;
}
[]
연산자로 범위를 벗어난 인덱스에 접근했을 때 예외 객체를 생성하고
doSomething()
함수의 catch()
내에서
main()
함수의 catch()
내에서
이렇게 확인했는데, 위에서 확인했던 내용대로 주소가 모두 달랐습니다. 그리고 doSomething()
에서 rethrow 를 throw;
로 변경하니 2번과 3번의 출력 주소가 같았습니다.
종합해보면 throw
로 전달되는 객체의 복사 여부는 catch()
의 &
에 따라 달라지는 것이 아니고, throw
에 피연산자가 생략되어 있냐 없냐로 달라지는 것 같더라구요. 그 마저도 throw;
처럼 작성하는 것은 rethrow할 때만 가능한 것 같았습니다.
다음으로 그렇다면 왜 catch()
에서 참조를 이용하는지 아래 링크를 참고했습니다.
참고해보니 예외 클래스가 상속 관계를 가지는 경우, 예외 객체를 초기화하는 과정에서 object slicing이 발생할 수 있기에 &
를 추가한다고 되어 있었습니다.
실제로 주소 출력을 했던 테스트 코드에서 doSomething()
에서는 throw;
로 작성하더라도 main()
의 catch()
에서 &
을 지우니 2번과 3번의 출력 주소가 달라졌습니다.
그리고 생각을 해보니 throw
는 expression이지 함수는 아니고, 더 찾아보니 'throwing by reference' 같은 개념은 존재하지도 않더라구요.
결론적으로 함수에서와 throw
문에서의 &
는 참조라는 같은 의미로 사용되지만, 함수의 매개변수에 사용하는 &
는 call by reference로 작동하기 위한 것이고 catch()
에서 사용하는 &
는 object slicing을 피하기 위한 것이라고 이해하면 될까요?
글이 조금 난잡할 수도 있는데, 읽어주셔서 감사합니다.
답변 2
3
안녕하세요, 질문&답변 도우미 Soobak 입니다.
네, 올바르게 이해하고 계십니다.
말씀해주신 내용 꼼꼼히 확인해보았으며, 저 또한 추가적으로 자료들을 찾아보며 확인해보았습니다.
요약하자면,
함수 매개변수에서의 &
: 참조로 매개변수를 전달하는 것을 의미, 원본 변수에 대한 참조를 통해 함수 내에서 해당 변수의 값을 변경할 수 있게 됩니다. (call by reference
)
catch
문에서의 &
: 예외 객체에 대한 복사본을 만들지 않고 직접 처리할 수 있으므로, 성능 상의 이점이 있습니다.
또한, 말씀하신 것처럼 객체 잘림(강의 12.9 객체 잘림과 reference wrapper 에서의 객체 잘림 부분 참고)문제를 방지할 수 있습니다.
throw
문에서의 &
: throw
문에서는 일반적으로 &
을 사용하지 않으며, 보통 예외 객체를 '값' 으로 throw
합니다.
(예외 객체를 포인터로 throw
하는 경우에는 &
을 사용할 수 있지만, 메모리 관리의 복잡성 증가 등의 문제로 권장되지 않는 방법이라고 합니다.)
1
새벽에 답변 달아주셔서 감사합니다. 저도 검색하다보니 여러가지 찾아보게 되더라구요. 그리고 마지막 문단에
throw
문에서&
을 사용한다는 오타가 있었네요...catch
문이라고 했어야 했네요. 감사합니다!