C++의 string 내부 배열 직접접근에 대한 의견
![익명 사용자의 이미지 익명 사용자의 이미지](/files/bluemarine_logo.png)
글쓴이: 익명 사용자 / 작성시간: 화, 2017/05/02 - 9:54오전
얼마전에 stackoverflow에서 어떤 사람이 c++ string의 array를 scanf로 읽을 수 있는지에 대한 질문을 올렸었습니다.
#include<iostream> #include <stdio.h> #include<string> using namespace std; int main() { string name; printf("\nEnter the name : "); scanf ("%[^\n]%*c", name); //it does not work //getline(cin, name); //it works fine cout<<name<<endl; return 0; }
일반적인 방법도 아니고 표준도 아니고 추천하지 않는 방법이지만,
string name; name.resize(50); // allocate the size of char scanf("%s", (char*)name.c_str()); // or scanf("%s", &name[0]);
라고 하면 되기는 될거라고 답변을 했었지요. (물론 이렇게 하면 안된다고도 했지요 ㅋㅋ)
사실 저렇게 하면 표준도 아니고 위험성이 생기는 것은 인지하고 있습니다.
하지만, string은 내부적으로 char array를 메모리로 잡고 있을테고, 모자라면 java처럼 2배로 늘려서 realloc하는 구조 등으로 사용할 것이라고 생각되었고, 정말 꼭 접근하지 않아야 하는 것이면 컴파일러가 에러를 뿜도록 (&name[0] 같은 경우는요) 하지 않았을가 싶기도 하고, 개인적으로 C++/C는 low level 메모리까지 접근하는 자유도에 강점(!)이 있지 않나라는 생각이 듭니다.
또한 완전히 비현실적인 시나리오는 아닌것이, 예를 들어 일반적인 경우는 아니지만, 고정되어 있는 string 내부의 char array를 복사하는 경우의 성능중심 서버라면 memcpy로 위와 유사하게 직접 string 사이즈를 잡고, 복사해버리는 방법이 더 빠를(!)수도 있겠지요. (범용적으로 쓸수는 없겠지만요)
위의 질문/답변 글은 downvote로 삭제되었는데, 이유는 unsafe하고 a violation하다는 이유였을 뿐 논리적인(!) 답변은 듣지 못했습니다.
혹시, 저렇게 사용하면 안되는 이유에 대한 논리적인 답변이 있을까요?
궁금해서 한번 여쭈어봅니다 ^^;
Forums:
제가 알기로는 std::vector는 연속된 메모리를
제가 알기로는 std::vector는 연속된 메모리를 가지는걸 보장하지만,
std::string은 연속된 메모리를 가지는걸 보장하지 않는다고 알고 있습니다.
아래 링크를 보시면 C++11에서는 연속된 메모리를 가지는 걸로 변경된 것 같네요.
http://stackoverflow.com/a/1986974
downvote 받아 마땅한 답변이로군요.
downvote 받아 마땅한 답변이로군요.
1. C++03까지는, string이 저장하는 문자열이 반드시 연속된 메모리라는 보장이 없습니다.
매우 naive한 생각입니다. C++ 라이브러리 구현에 따라서 string은 참조 카운팅(Reference counting)을 할 수도 있고, 짧은 문자열에 대해서 동적 할당을 하지 않고 객체 멤버로 잡아 놓은 배열에 저장하는 단문자열 최적화(Small string optimization)를 적용했을 수도 있기 때문이지요.
위 두 가지 최적화 사례는 C++03 표준을 준수하는 string 객체가 자기만의 동적 할당된 문자 배열을 갖지 않게 되는 주요한 사례입니다. 실제로 위와 같은 구현 사례가 있느냐고 묻는다면, Scott Meyers의 ≪Effective STL≫ Item 15: Be aware of variations in string implementations.에 자세한 설명과 case study가 나와 있습니다.
2. 설령 C++11을 쓴다던가 하여 string이 연속된 메모리를 담고 있더라도, c_str이 반환한 배열을 변경해서는 안 됩니다.
string의 멤버 함수 c_str의 반환 타입이 무엇인가요? C++03와 C++11을 막론하고, const charT*입니다. 그걸 알고 계시니 (char *)로 강제로 캐스팅해서 쓰셨겠지요.
이런 식으로 클래스 인터페이스를 무시하여 강제로 const를 벗겨내고 쓰면 컴파일러가 별 말 안 해주는 게 C++의 자유도입니다. 임의로 char * 같은 low level pointer를 잡아서 메모리 어디든 읽고 쓸 수 있는 것도 C++의 자유도가 맞고요.
자유의 대가는? 책임이죠. C++는 프로그래머가 이런 식으로 막나갈 경우 런타임에 대해 아무런 보장도 안 해줍니다. 프로그래머가 다 책임지고 가야 하는 겁니다. 무슨 일이 생길 줄 알고 이런 식으로 무슨 일이 있어도 내가 다 책임지겠다는 식의 용감무쌍한 코딩을 할 수 있을지 모르겠습니다만.
그런 측면에서 보면 &name[0]를 이용한 코드는 조금 더 양호한 편입니다. 최소한 타입 문제는 없지요. 그래도 후술할 문제가 여전히 남아 있기는 합니다.
3. 위의 2.가 만족스러울 만큼 구체적이지 않다고 생각하신다면, 좀 더 구체적으로 말씀드리지요.
일단 위험합니다. scanf가 딱 49문자(+NUL 해서 50문자)까지만 입력받는다는 보장이 어디 있나요? 그보다 더 길어지면 string 객체에 할당해 놓은 버퍼를 넘어가겠지요. string 객체가 제공하는 public 메서드들(e.g., append)을 사용하면 적절히 버퍼를 확장해 주겠지만, 이 경우엔 그런 걸 기대할 수 없으니까요. 물론 애초에 %49s 하면 되긴 되지요. 근데 scanf가 받아올 문자의 길이가 처음부터 정해져 있었다면, 꼭 이렇게 하지 않았어도 대안은 많았습니다.
그건 그렇고, scanf가 49문자(+NUL 해서 50문자)를 못 채우면, 그 땐 어떻게 될까요. scanf는 일찌감치 NUL문자를 박고 끝내겠죠. 물론 string 객체가 NUL문자를 포함하지 말라는 법은 없긴 합니다만, 그 직후에 size 멤버를 호출하면 어떤 값을 반환할까요. 실제로 담겨 있는 문자와 상관없이 50을 반환하지 않을까요? 기타 size를 참조하는 다른 모든 멤버 함수들도 마찬가지입니다. 최악의 경우 잘못 동작하고, 최선의 경우라도 성능이 떨어지겠죠. 물론 이 문제 역시 scanf 이후 strlen이라도 먹여서 올바른 길이로 resize 해주면 되기는 됩니다.
역시나, 이렇게까지 번거롭게 뒷처리를 해서 쓸 생각이었으면 애초부터 다른 대안이 있었습니다.
표준에서 보장하지 않는 조건을 임의로 가정해서 위험천만한 코딩을 하는 건 절대 함부로 권장되어서는 안 됩니다. 설령 그렇게 하는 게 꼭 필요하더라도, 자기가 하고 있는 게 정확히 무슨 일인지 알고 있는 숙련된 프로그래머에 한해서만 허용되어야 합니다. 그런 종류의 일을 함부로 권장하는 답변이 stack overflow 같은 데 올라와 있다면 downvote를 하지 않고 넘어가는 게 태만이라고 할 수도 있겠네요.
애초에 string이 있는 이유가 무엇일까요. C언어를 오래 써 보신 분이라면 아시겠지만, 가변 길이의 문자 배열을 직접 다루는 게 하도 골치가 아파서 그렇습니다. 온갖 다양한 종류의 실수들이 많이 일어났고 치명적인 결과를 낳은 것들도 있었어요. 그런 걸 안전하게 할 수 있도록 클래스로 감싸고 public 메서드들을 만들어 놓은 건데, 내부 배열의 포인터를 얻어내서 scanf로 %s를 받는데 쓰신다니, 솔직히 아이러니가 아닐 수 없습니다. 누가 그런 코드를 좋게 봐 주겠습니까.
ktd2004님이 말씀해주신 것처럼 c++11에서는
ktd2004님이 말씀해주신 것처럼 c++11에서는 표준이 보장하기 때문에 말씀하신 방법들을 사용할 수 있습니다. 하지만 질문자님이 제시하신 논리는 일반적으로 적절하지 않습니다.
> 하지만, string은 내부적으로 char array를 메모리로 잡고 있을테고, 모자라면 java처럼 2배로 늘려서 realloc하는 구조 등으로 사용할 것이라고 생각되었고, 정말 꼭 접근하지 않아야 하는 것이면 컴파일러가 에러를 뿜도록 (&name[0] 같은 경우는요) 하지 않았을가 싶기도 하고, 개인적으로 C++/C는 low level 메모리까지 접근하는 자유도에 강점(!)이 있지 않나라는 생각이 듭니다.
매우 매우 나쁜 생각입니다 :) implementation detail 에 의존하는 프로그래밍은 피해야합니다. 라이브러리 버젼이 바뀌기만 해도 문제가 생길 수 있습니다. 테스트도 어렵구요. "low level 메모리까지 접근하는 자유도"는 필요한 곳에 써야지 아무데나 쓰면 안되겠지요. "unsafe 하기때문에 사용하지 말아야한다"가 바로 "논리적인" 답변입니다.
> "정말 꼭 접근하지 않아야 하는 것이면 컴파일러가 에러를 뿜도록"
C++은 그런 거 잘 못합니다. C에 비해서는 안전장치가 많기는 하지만 C++도 그리 엄격한 언어는 아닙니다.
감사합니다.
답변주신 분들 정말 납득이 되고 공부가 됩니다.
게다가 단문자열 최적화(Small string optimization)는 정말 생각지도 못한 부분이네요.
"unsafe 하기때문에 사용하지 말아야한다"가 바로 "논리적인" 답변입니다.라는 말씀등도 세겨 듣겠습니다.
정말 감사합니다
댓글 달기