C++만 사용하고 싶어도 태생적 한계 때문에 C API 와의 호환에 대하여 항상 염두해 둬야 하는게 C++ 개발자들의 숙명입니다. 바이트배열로 직렬화 하는 부분에서도 이러한 랑데뷰가 발생하는데 이번 포스트에서 이에 대한 내용을 정리 하려고 합니다. 구체적으로는 int 형을 char* 로 바꾸는 방식에 대해서 살펴 봅니다.

* 직접 메모리를 할당하는 경우에는 메모리 해제하는 부분등에서 오류를 범할 확률이 높아질테고, std::vector , std::string 등을 사용하면 그런 부분은 해결되나 라이브러리에 대한 이해 없이 사용 할 경우 미묘한 버그를 만들어 내서 더욱 찾기가 어렵게 만들 수 도 있습니다. 모든게 좋은 건 세상에 없지요~ 트레이드오프!! 

C  memcpy

unsigned char * arr= (unsigned char*)malloc(8);
uint64_t x = 200321;
memcpy(arr, &x, 8);

- memcpy를 이용하면 간단히(형변환없이) uint64 타입의 숫자를 바이트배열로 직렬화 할 수 있습니다.
- memcpy는 무엇이 매개변수로 들어 오던 void* 로 받아서 바이트배열로 처리 하니까요.
- 동적으로 할당한 메모리를 해제시켜주는 것을 잊지 마세요. 
- unsigned char arr[8]; 이렇게 스택에 만들어주는게 낫겠죠.

unsigned char * arr = (unsigned char*)malloc(100);
uint64_t x = 200321;
memcpy(arr, &x, 8);

uint32_t y = 52;
memcpy(arr + 8 , &y, 4);

- 이렇게 배열을 크게 잡아서 다양한 타입의 직렬화를 구성할 수도 있을 것입니다.

C++  std::vector 

uint64_t x = 200321;
std::vector<uint8_t> arr(8);
std::copy((uint8_t*)&x, (uint8_t*)&x + 8, &arr[0]);

- vector의 시작 포인터는 &arr[0]; 과 같이 추출할 수 있습니다.
- std::copy 를 이용합니다. 성능은 memcpy와 비슷하며 타입에 대해 좀 더 따지게 됩니다.

- 만약 std::copy(&x, &x + 8, &arr[0]); 이렇게 한다면?? 재앙이 일어 날 겁니다. 오류가 생기진 않죠. 따라서 해결하기 어렵게 될 수 있습니다. 여기서 &x + 8 은 8바이트 뒤의 주소가 아니라, 64바이트가 됩니다.
- vector 는 자동으로 메모리 관리를 해주므로 더 많은 메모리가 필요하면 자동으로 추가 됩니다.
- vector 의 재할당 때문에 느려지는 속도가 염려스러우면 reserve 로 미리 많이 잡아 두면 됩니다. 

std::vector<uint8_t> vec(100);

uint64_t  _blockNum1 = 543210;
std::copy((uint8_t*)&_blockNum1, (uint8_t*)&_blockNum1 + 8, &vec[0]);

uint32_t  _blockNum2 = 12;
std::copy((uint8_t*)&_blockNum2, (uint8_t*)&_blockNum2 + 4, &vec[8]);

- 이렇게 배열을 미리 크게 잡아두고 다양한 타입을 직렬화 할 수 도 있습니다.

다시 숫자타입으로 돌리려면 아래와 같이 하면 될 거 같습니다.
방법1)

uint64_t x = *(uint64_t*)(&vec[0]);
std::cout << "result : " <<  std::setprecision(10) <<  x << std::endl;

방법2)

void toUint64(uint8_t * c , size_t n) {
   uint64_t i = (c[7] << 56) | (c[6] << 48) | (c[5] << 40) | (c[4]) << 32 | (c[3]) << 24 | (c[2]) << 16 | (c[1]) << 8 | (c[0]);
   std::cout << "result : " << i  << std::endl;
}


C++ std::string 

int x = 200321;
std::string arr;
arr.append((const char*)&x, sizeof(int));

- std::string 또한 훌륭히 바이트배열을 조작하는데 사용 할 수 있습니다.
- append 에는 바이트포인트와 타입의 길이가 들어 갑니다.

이것은 아래와 같이 다시 int로 되돌릴 수 있을 것입니다.

const char* pointer = arr.c_str();
int y = *(int*)(pointer);

- c_str()은 vector에는 없고 string에만 있는 것으로 바이트 포인트를 리턴해 줍니다.
-  &arr[0]; 해줘도 에러는 안나겠지만 지양해야합니다. vector와 다르게 string은 연속된 주소를 할당하지 않을 수 있습니다.

// 인코딩
std::setprecision(10);
double x = 2012621.12;
std::cout <<  "start : " << x << std::endl;
std::string arr;
arr.clear();
arr.append((const char*)&x, sizeof(double));

// 디코딩 
const char* pointer = arr.c_str();
double y = *(double*)(pointer);
std::cout << "result : " << y << std::endl;

- 이렇게 어떤 타입이든 직렬화를 할 수 있습니다. string, wstring, map 등등


p.s

Effective STL 의 챕터 2는 아래와 같은 항목이 있습니다. 참고 하시구요. 
Chapter 2 vector와 string

항목 13 : 동적으로 할당한 배열보다는 vector와 string이 낫다.
항목 14 : reserve는 필요 없이 메모리가 재할당되는 것을 막아 준다
항목 15 : 잊지 말자! string은 여러 가지 방식으로 구현되어 있다는 사실을...
항목 16 : 기존의 C API에 vector와 string을 넘기는 방법을 알아두자
항목 17 : 쓸데없이 남은 용량은 "바꿔치기(swap) 묘수"를 써서 없애 버리자
항목 18 : vector 보기를 돌같이 하자

사족: 다시 C++을 시작 하면서 세부적이진 않지만 기본적으로 기억해 두어야할 지침 50가지를 정리해 보았습니다. C++을 처음 시작하는 개발자들에게도 도움이 되지 않을까 싶습니다. 해당 내용 회색 글자는 팀에 의해 선택될 여기가 있다고 보는 방식입니다. 기억하기 쉽도록 경구만 썼으며 자세한 내용은 구글링과 젤 마지막 레퍼런스를 통해 확인 하시길 바랍니다.

C++ 50 계명 - 이것만은 기억하자.


일반 

1. immutable / const 은 디폴트로 적극적으로 사용한다.
2. 다형성을 가진 기본 클래스는 소멸자를 반드시 가상 소멸자로 만든다.
3. 값을 그대로 매개변수로 넣지말고, 대개 상수객체 참조자로 전달한다.
4. 변수 정의를 상단에 몰아서 하지 않는다. 사용 되는 곳 근처에서~
5. 상속보다는 합성을 사용한다. 
6. warning 을 보면 자기가 없앤다. 팀원들이 보지 않게 한다. 
7. RAAI를 항상 인식하되, 표준에서 제공해주는 것으로 되는지 확인한다.
8. C++ 은 null 포인터를 삭제해도 문제가 없다. 다만 개발시 null assert 는 많으면 많을 수록 좋다.
9. C++을 사용한다면 Class 만드는 것을 겁내지 마라. 모든것을 클래스/객체로 만든다는 생각으로 코딩한다. C++ 에서 Class는 다양한 역할을 할 수 있다. 그런게 싫다면 자바,C++  하지말고 C/Golang으로 가자. (38번참조) 
10. auto/ override /nullptr / constexpr / move / forward / atomic 키워드를 사용한다.
11. namespaces를 적극적으로 사용하며, using 은 사용하지 않는다. std:: 를 직접 붙혀준다.
12. {} 중괄호 초기화를 적극적으로 이용한다. vector<int> vec(10,20); 과 vector<int> vec{10,20}; 의 차이를 구별한다.
 initializer_list 에 대해 이해한다. 

STL

13. STL을 신뢰하고 사용한다. (개인적으론 boost 도) 
14. STL 컨테이너는 쓰레드 안전하지 않다고 각인하자.  C++11에도 여전히 쓰레드 안전한 컨테이너는 없다.( http://libcds.sourceforge.net/  이런 대체제가 있다. boost에도)
15. 많은 경우 동적으로 할당한 배열보다는 vector와 string이 낫다. (std::copy가 memcpy보다 느리지 않다)
16. 정렬이 필요 없는 맵은 C++11의 unordered_map 을 사용한다.
17. 요소를 정말로 제거하고자 한다면 remove 류의 알고리즘에는 꼭 erase를 붙여 사용하자 
18. STL 알고리즘을 적극적으로 이용한다. 

Smart Pointer

19. new / delete 를 직접하는 시대는 지났다.
20. unique_ptr 를 디폴트로 사용하자. unique_ptr로 충분하게 설계하자.
21. make_shared 로 초기화 하자.(deleter 넣어줘야 할 경우 제외) 
22. shared_ptr 나 unique_ptr 사용시 raw pointer를 별개로 생성/소멸하지 말자. 
23. 배열을 스마트 포인터로 만들었을 경우 반드시 custom deleter 를 생성자에 넣어주자.
24. shared_ptr 사용시 순환 참조가 되는지 확인하자. (weak_ptr로 해결)
25. std::move 로 스마트포인터를 이동시켜야 할 경우가 있을 때 조심 조심.
26. 위의 룰을 어겨야 할 경우가 발생시 팀원들에게 알린다.

Move vs Copy

27. 이제 move가 디폴트고 copy가 쵸이스다. 항상 연구하자. move를 사용 할 순 없는지..
28.  std::move는 원본이 없어진다는 것을 의미한다. 원본이 필요 없으면 이동하자. 

Type

29. 이왕 C++ 쓰는거 타입을 구체적으로 명시하고 타입에 안전한 코딩을 한다.
30.   int8_t, int64_t, uint32_t 와 같은 구체적 표준타입을 사용한다. 
31.  std::memcpy, std::memmove, std::memset, std::memcmp, 대신해 std::copy, std::move, std::fill, and std::equal[1] 를 사용하자.타입안정적이며 성능도 더 느리지 않으며 더 나아질 여지도 있다.

예외

32. 예외/에러는 최대한 가까운 곳에서 처리한다.(참고: 예외 처리에 대한 6가지 화두)
33.   예외를 상위 전가 시키지 말고, 리턴 값으로 처리한다. 
34.  모든 리턴 값은 에러처리로 사용한다.
35.  std::optional 을 적극적으로 이용한다.

예외처리는 너무 다양한 시각이 존재하므로 팀 설계 철학에 따라 갑니다. 개인적으로는 c/golang 방식으로 최대란 리턴값으로 가까운 곳에서 처리하는게 나은거 같습니다. 아마 예외처리에 대해 전 팀원들이 충분히 숙지/약속되어 있는 상태라면 자바처럼 복잡한/강제적인 예외 시스템을 사용하는 것도 괜찮긴 합니다만..

성능

36. 섣부른 최적화는 만원의 악 
37. 직접 메모리풀/쓰레드풀을 만드는 대신 대체제가 있는지 확인하다. 직접 만들어야 할 필요가 있을 경우 철저한 테스트가 필요하다. 
38. 인라인을 항상 고려해 본다. 
39. 아주 단순한 컨테이너/알고리즘인데 STL과 아주 큰 성능상 차이가 있으며, 그 성능이 해당 솔루션의 철학에 중요하다면 그때서야 자신의 컨테이너를 만든다.
40. 객체 생성 비용에 대한 문제가 생길 수 있다는 것만 기억하되, 최적화(BMT) 전에는 신경쓰지말아라.(9번참조)

디자인패턴/리팩토링

41. 메소드의 크기를 작게하는건 기본 중의 기본. 
42. 클래스의 책임 범위를 최소화 하며, 외부에서 로직을 주입받아 사용한다.
43. 싱글턴 패턴은 가급적 사용하지 말자. 
44. 적어도 어댑터,커맨드,전략,컴포지트,옵저버패턴은 대화가 통하게 알아두자.

동시성

45. thread 보다는 task, task 보다는 추상층 높은 아키텍터 선택(Actor, CSP)
46. 락 없는 동시성 개발을 지향한다.
47. Read/Write Lock을 선택한다.
48. 이미 존재하는 쓰레드 안전한 자료구조(컬렉션)를 사용한다. 
49. 멀티쓰레드/비동기가 무조건 빠르다는 상식을 버린다.

   예) https://hamait.tistory.com/960  https://hamait.tistory.com/839

50. 표준 병렬알고리즘을 검토한다.

결론

C++을 굳이 사용한다면 그에 걸맞는 지식을 공부하라. 로우레벨을 잘 활용 할 수 있는 수많은 지식들을 익혀야하며 그 와 동시에 OOP의 철학까지 함께 익혀야하는 정말 조심스럽게 다루어야하며 공부해야할게 많은 언어이다. 트레이드 오프를 명심하라. 필자는 C++ 경력 동안 많은 코딩과 C++서적도 20권 이상 독파했지만 대부분 다른 언어/개발 하면서 그 정수를 많이 까먹었고, 미묘한 실수를 범할 여지가 있어서 다시 체득하려면 비용이 또 따를 것이다. 따라서 애초에 이런 C++에 들이는 비용(공부비용, 수동관리에 의한 오류발생 비용,각종 타이밍 버그) 을 줄여주는 Java,Go,Rust, Python,Clojure 같은 언어를 사용하여 만들어야하는 솔루션에 집중하는 시간을 늘리는게 많은 분야에서 유리하다고 본다. 속도먹는 하마 부분만 모듈화를 한다던가. 이제 C/C++만 돌아가는 환경/C/C++에 대한 자료뿐이 없는 분야/ 0.1ms의 속도라도 줄여야하는 극한의 상황이 아니고서는 다른 언어로 눈을 돌려보는게 어떨까.



레퍼런스:

https://www.acodersjourney.com/top-10-dumb-mistakes-avoid-c-11-smart-pointers/
https://www.oreilly.com/ideas/2-major-reasons-why-modern-c-is-a-performance-beast
http://www.modernescpp.com/index.php/copy-versus-move-semantic-a-few-numbers
http://maintainablecode.logdown.com/posts/159916-memcpy-memmove-and-memset-are-deprecated
Effective C++ / Effective Modern C++ / Effective STL  / Efficient C++ 

C++ 경우는 정의되지 않은 행동을 합니다. (제작자에 따라서 다름) 따라서 항상 검사를 미리하고 사용해야합니다.

 

// queue::empty

#include <iostream>       // std::cout
#include <queue>          // std::queue

int main ()
{
  std::queue<string> myqueue;
  myqueue.front();  // <--------------- what happen ? 
 
  return 0; 

} 

 

  P.S

 

  제가 처음 프로그래밍 배울때 만든 큐는 리턴값이 bool  형이고  매개변수를 통해서 값을 반환하였습니다.

  자바의 경우는 null 객체를 반환합니다.

  Scala 언어의 경우는 예외를 반환합니다.

 

  무엇이 좋은지는 각자 판단을 ~!



안전한 C++ 라이프를 위한 스마트포인터에 대해 정리를 해봤습니다. (2012년 현재) 

마지막 한줌의 속도라도 짜내야하는 그런 부분 제외하고는 웬만하면 C++ 말고 다른것을 사용하라고 조언드리고 싶고, Rust 의 빠른 발전을 기대합니다.  



1. Scoped_ptr (boost)

0. 복사불가 단일 소유자
1. 
한마디로 scoped_ptr  복사할  없는 auto_ptr 이다
2. 
 가볍습니다.
3. 
제한적이다. ( 함수 내에서만 동적할당해서 쓰다가 함수를 끝낼 때는 삭제해야 하는 임시 객체를 처리할 때도 유용하게 쓰일  있습니다)
4. 
 스마트포인터를 포함하고있는 클래스도 복사불가입니다.
5. "
자원 획득은 초기화 이다." 만을 위해 존재한다레퍼런스 카운팅이 없고소유권공유,이전문제도 없다.
6. 
명확한 쓰임새를 위해서 비전에서 이것으로 교체하는게 좋을듯합니다.
7. STL 
컬렉션과 함께 사용불가


2. Scoped_array (boost)

0. 동적으로 할당한 배열에 대해 사용
1. 
복사불가 단일 소유자
2. 
동적인 배열이 필요하다면 std::vector라는 훌륭한 대체 수단이 이미 존재하는 관계로
 scoped_array 사용 목적은 주로 오래되고 복잡한 코드를 유지 보수하면서 혹시나 delete[] 잊을 수도 있는 후임자를 위해 사용해라가 되겠습니다.


3. auto_ptr (C++98)

0. 복사가능 단일 소유자
1. 
한마디로 복사할수있는 Scoped_ptr 입니다.
2. A->B 
 복사하고나서 A  소멸되면 B 스마트포인터는 빈곳을 가르킨다면 대략 낭패입니다.
3. 2
 문제를 해결하고자 auto_ptr에서는 소유권을 이전시켜버립니다. A  복사하는순간 쓸모없어지는것이지요.
4. 
레퍼런스카운팅을 하지 않는다. (단지 소유권 이전으로 단일 소유자만 존재)
5. STL 
컬렉션과 함께 사용불가


4. shared_ptr (C++TR)

: 0. 복사가능 공유소유자
: 1. 
레퍼런스 카운팅을 통해 공유를한다가장 마지막 소유자가 제거될때까지 힙에 존재합니다.
: 2. STL 
컬렉션과 함께 사용가능
: 3. 
가장 무겁다.
: 4. Copy on write 
 아니다한쪽에서 값이 바뀌면  모든 소유자가  바뀜.


5. weak_ptr (boost)

: 0.weak_ptr  share_ptr  초기화 되어야 한다는 제약.( 이것은 share_ptr 관련되어서만 사용해야함.)
: 1.weak_ptr
 객체의 생명주기에 영향을 주지 않는다. (레퍼런스 카운팅에 영향을 안준다는뜻)
: 2.enable_shared_from_this<>
 내부적으로 weak_ptr 이용하여 this 포인터의 shared_ptr 전달 문제를 해결하고 있다.
: 3.
보통 shared_ptr  circular reference 문제를 해결하기위해 사용된다.

6. unique_ptr (C++11)

: 0. auto_ptr shared_ptr 사이의 절충안
: 1. unique_ptr
 기본적으로 auto_ptr 유사하게 소유권의 이전에 기반한 동작을 한다
: 2. 
그러나 일반 복사 생성대입 연산이 아닌 C++11에서 새롭게 추가된 R-value reference 이용한다는 것이다.
: 3. C++11
에서 STL 컨테이너에서도 효율성을 위해 내부적인 복사  대입 동작은 전부 R-value reference 이용하도록 바뀌었는데
일반 복사  대입 연산자를 막아 버리는 대신 R-value reference 이용한 복사  대입 연산자만 정의한다면 STL에서도 사용할  있는 auto_ptr 생긴다는 것이 unique_ptr 의미이다.
: 4. STL 
컨테이너에서도 사용할  있다.
: 5. raw pointer
 비해 추가적인 오버헤드도 없는 스마트 포인터라   있다의미적인 뚜렷함과 안전한 사용을 위해 대부분의 암시적인 변환이 막혀 있고일반적인 대입  복사가 막혀있으므로 
unique_ptr 
끼리의 대입에는 std::move 함수를 명시적으로 사용해야 하는  코딩량이 다소 늘어난다는 불편함이 있으나안전하고 속도개선의 코딩을 위해서라면  정도는 충분히 감수할  하다.

'C++ (비공개)' 카테고리의 다른 글

C API 와 C++(STL) 호환에 관한 팁  (0) 2019.03.24
C++ 50 계명 - 이것만은 기억하자.  (1) 2019.03.19

+ Recent posts