이 문서는 제가 트위터에 냈던 문제의 해설입니다.
#include <vector>
using std::vector;
int main() {
vector<int> a = { 1, 2, 3 };
int& b = a[1];
a.push_back(4);
b = 0;
// 이때 `a[1]` 의 값은?
}스포주의
알 수 없다 입니다. UsB가 제일 먼저 일어나고, 그거에 따라 UB가 일어날수도 있습니다.
이 문제에는 함정이 두개있습니다.
-
벡터의
.size()가.capacity()와 같은 상황에서.push_back()을 호출하면 기존 원소들에 달려있던 레퍼런스들은 전부 invalidate 됩니다. 그래서 아래 코드는 확실한 UB입니다.vector<int> a = { 1, 2, 3 }; // a.size() == a.capacity() 라고 가정 int& b = a[1]; a.push_back(4); b = 0;
-
근데 C++ 표준은
.capacity()가 어떤 값이 되어야한다고 전혀 정의를 안해놨습니다. 대부분의 구현체들은vector<int> a = { 1, 2, 3 };코드 직후에.size()와.capacity()가 모두 3으로 같긴 하겠지만, 그냥 구현체가 그럴뿐이지 표준에 명시되지 않았습니다.심지어 C++ 표준은
.shrink_to_fit()을 호출한 뒤에.capacity()의 값이 줄어들어야한다는것조차도 정의하지 않았습니다.그래서 이런식의 문제는 코드만 보고서는 어느때에
a.size() == a.capacity()를 만족할지 전혀 알수가 없습니다. 그래서 Unspecified 입니다.
Undefined Behavior, Unspecified Behavior, Implemenation-defined Behavior는 편의상 각각 UB, UsB, IDB로 줄여부릅니다. 그리고 세 용어의 정의는 C++ 표준 거의 첫페이지에 나와있습니다.
C++ 표준은 https://isocpp.org/std/the-standard 에서 표준제정 직전의 Working Draft를 받아서 보실 수 있으십니다.
N4296을 기준으로, UB, UsB, IDB 세 용어의 정의는 각각 아래와 같습니다.
-
undefined behavior
1.3.24 [defns.undefined]behavior for which this International Standard imposes no requirements
-
unspecified behavior
1.3.25 [defns.unspecified]behavior, for a well-formed program construct and correct data, that depends on the implementation
-
implementation-defined behavior
1.3.10 [defns.impl.defined]behavior, for a well-formed program construct and correct data, that depends on the implementation and that each implementation documents
UB는 표준이 '이 동작은 어떠해야한다'고 정의하지 않은 경우입니다. UB의 제일 대표적인 경우가 Dereferencing of null pointer입니다.
int *ptr = nullptr;
*ptr = 100; // <- UB두번째줄에서 세그폴트를 일으키고 죽든, 널포인터 익셉션 경고메세지를 띄우고 죽든, 죽지않고 진행하든, 컴퓨터를 포맷하든, 로봇혁명을 일으키든 모두 C++ 표준을 거스르지 않는 동작입니다.
당연히 UB에 의존해선 안됩니다. UB가 어떠한 sane한 값을 내놓는것처럼 보여도 보아선 안됩니다. 컴파일러가 기분좋은 결과를 들려줘도 들어선 안됩니다. UB가 믿음직해보여도 믿어선 안됩니다.
UsB는 표준이 '이 동작은 어떠하거나, 어떠하거나, 어떠해야한다' 이런식으로 선택지를 제공한 경우입니다. UsB의 제일 대표적인 사례는 (C++14 기준) 파라미터 eval 순서입니다.
function_call(foo(), bar());foo()가 먼저 호출되고 그 다음bar()가 호출됨 - OK!bar()가 먼저 호출되고 그 다음foo()가 호출됨 - OK!foo()만 두번 부름 - 표준에 어긋남- 일본을 공격함 - 표준에 어긋남
구현체가 고를 수 있는 선택지가 유한하지 않을수도 있습니다. 방금 여러분이 푸신 문제가 그 경우에 해당합니다.
vector<int> a = { 1, 2, 3 };
// a.capacity() 의 값은?- 1 - 표준에 어긋남
- 2 - 표준에 어긋남
- 3 - OK!
- 4 - OK!
- 5 - OK!
- ...
이경우 C++ 표준은 a.capacity()의 값이 a.size()보다 작아서는 안된다고만
써놨지 어떠한 값을 가져야한다고 써놓지는 않았습니다. 그래서 UsB 입니다.
대부분의경우 UsB에 의존하여 프로그래밍하면 좋지 않습니다.
IDB는 UsB의 하위집합입니다. C++ 표준은 UsB중 몇몇개를 IDB로 지정하여 구현체가 어느 선택지를 골랐는지 명확하게 문서화하여, 프로그래머가 그 동작에 의존할 수 있도록 할것을 명시하고있습니다.
제일 대표적인 IDB가 size_t의 크기입니다. C++ 표준에는 size_t의 크기가 그냥
"large enough to contain the size in bytes of any object" 라고만 써있지
어떠해야한다고는 써있지 않습니다. 하지만 동시에 "implementation-defined"임을
명확하게 명시하고있고, 모든 컴파일러 구현체는 size_t의 크기가 얼마인지 전부
명확하게 써야합니다.
또다른 IDB가 #pragma입니다. C++ 표준에는 Pragma directive라고 해서
#pragma blabla blabla 를 코드 중간에 쓸 수 있음을 명확하게 명시하였지만, 그
동작이 어떠할지는 구현체에 맡겼습니다 (behave in an implementation-defined
manner)
UsB와는 다르게, IDB는 필요할때엔 사용해야합니다. size_t의 크기가 얼마인지도
가정하지 못한다면 프로그래밍하기 너무 힘들겠죠? 그래도 코드가 portable하게
동작하길 원한다면 IDB에도 의존하지 않는것이 좋습니다. 예를들어 32비트와 64비트
모두에서 올바르게 컴파일되길 원한다면, size_t의 크기가 몇이라고 가정하기보다
sizeof(size_t)에 의존하는것이 좋겠죠.
C++에 IDB는 몇개 없습니다. C++ 표준 맨 마지막장에 컴파일러 만드는 사람들 보기 편하라고 IDB만 추려놓은 목록이 있는데, 한번 보시면 일반 프로그래머가 C++을 쓰다가 IDB에 의존할 일이 별로 없음을 아시게될겁니다.
안녕하세요. 작성해주신 글을 읽어보던 중 궁금한 내용이 있어서 comment 남깁니다.
글 초반부에 '파라미터의 평가 순서는 더 이상 UsB가 아니다'라는 언급과 링크해주신 문서가 C++17의 accepted proposal인 것으로 아는데요.
이 문서를 살펴보아도 함수 파라미터의 평가 순서는 여전히 UsB가 맞는 것으로 생각되어서 제가 잘못 이해한 부분이 있는지 여쭙고자 합니다.
위 내용 중 4번을 보면 함수 a의 파라미터인
b1,b2,b3간의 평가 순서는 여전히 UsB인 것으로 이해됩니다.왜냐하면, 문장을 시작할 때 '~~ are evaluated in the order a, then b, then c, then d:' 라고 표현했기 때문입니다.
다소 헷갈리게 쓰여있긴 하지만, 만약 함수 파라미터의 평가 순서를 LTR로 보장하자는 proposal이었다면
a(b, c, d)로 표기했을 것입니다.이 proposal에서 제안하는 것 중 하나는 postfix-expression의 평가 순서를 LTR로 하자는 것입니다. 그러므로,
a(b1, b2, b3)는 함수를 먼저 평가하고 파라미터를 평가하자는 의미로 해석됩니다. cppreference에서도In a function-call expression, the expression that names the function is sequenced before every argument expression and every default argument.라는 표현이 있는 것을 보면 요쪽으로 해석하는게 맞을 것 같습니다.하여, 작성해주신 글의 UsB 단락에서 C++14 기준이라는 단서는 없어도 될 것 같다(C++17이후에서도 이건 UsB이므로)는 것이 제 생각인데요.
지현님께서는 어떻게 생각하시는지 답변을 구하고 싶습니다.
감사합니다 :)