2020년 9월 17일 목요일

Github merge 옵션 관련

Github에서 Pull request를 merge 시 git에서 제공하는 여러 merge option을 팀 공유를 위해 간략히 정리함




Github에서 Pull request merge 시 아래 가이드와 같은 merge 방법이 있음.

- Merge pull request

- Squash and merge

- Rebase and merge 


Merging a pull request

 : https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/merging-a-pull-request


각 방법들은 git에서 제공하는 merge 방법으로 아래 가이드를 보면 이해하기가 쉬울 듯함.

About merge methods on GitHub  

https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/about-merge-methods-on-github

https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-request-merges

브랜치 통합하기

 : https://backlog.com/git-tutorial/kr/stepup/stepup1_4.html

3.2 Git Branching - Basic Branching and Merging 

 : https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
 : https://git-scm.com/docs/git-merge

3.6 Git Branching - Rebasing

 : https://git-scm.com/book/en/v2/Git-Branching-Rebasing
 : https://git-scm.com/docs/git-rebase



GitHub 가이드가 가장 깔끔하게 설명을 하고 있어 간략히 발췌하면


Merge pull request는 

 . Pull request의 모든 commit을 default branch로 추가하고 merge commit을 생성한다.
 . git merge 시 --no-ff option을 사용하는 것이라 merge commit을 생성하게 되는 것이다.
   : 이 옵션은 주로 특정 branch나 개발 history를 유지하려는 경우 사용됨.
   : https://stackoverflow.com/a/14865661
   : https://koreabigname.tistory.com/m/65


아래 그림을 보면 feature branch에서는 default branch 기반으로 D,E를 생성하였기 때문에 A,B,C 다음 D,E를 추가하면 되지만 merge commit을 생성하여 default branch에 추가하게 된다. 




Squash and merge

 . 용어와 같이 pull request의 commit을 하나로 모아 default branch에 추가하여 merge하게 된다. 
 . Repository 설정에서 squash merging을 허용해야 한다.
 . 수정/개발 hisotry가 중요하지 않다면 이 옵션을 사용하여 default branch를 깔끔하게 관리할 수도 있겠음.




Rebase and merge

 . Pull request의 모든 commit이 head branch에 merge commit 없이 차례로 추가된다.
 . Git repository에서 rebase merging이 허용되어야 한다.
 . Github의 rebase는 git rebase와 다른 점이 있는데 git rebase는 committer 정보와 commit SHA를 변경하지 않지만 Github는 변경하는 점이 다르다고 함. 
 . Github에서 자동으로 Rebase and merge가 되지 않는다면 command line에서 rebase를 수행하여 conflict를 해결하고 force push를 해야 함.





다만 rebase 수행 시 조심해야 할 점은

 . 작업 branch가 default branch에서 분기되어 오래되었다면 merge conflict이 더 자주 발생할 수 있음. 
   > default branch에 대해 자주 rebase하고 commit하면 해결 될 수 있음.
   > 솔직히 Github에서 branch는 필요할 때 마다 만들어 사용하는게 맞는 것 같다.

 . Interactive rebase 작업 도중 commit history를 잃어버릴 수 있음. 
   > git reflog를 이용하여 복원하거나 rebase 작업을 취소할 수 있다고 함.

Atlassian Bitbucket git rebase



마지막으로 아래 글은 Git 사용자라면 한 번쯤 봤을 정도로 고전인 article으로

Project 수행 시 어떻게 branch를 관리하면 좋을지에 대한 내용이니 꼭 읽어보고 알아두어야 함.

A successful Git branching model

: https://nvie.com/posts/a-successful-git-branching-model/





2020년 8월 18일 화요일

Github에서 협업 개발 시 fork 기반 개발과 branch 기반 개발 사용 관련

아주 사소한 것이지만 공유를 위해 대충 정리해봄.
S/W 개발은 몇 명의 팀원들이 Github을 사용하여 협업을 하는데 코드 반영을 위한 방법을 두고 팀원 간 이견이 있는 경우가 있는데 정답이 있는 것이 아니고 상황에 맞춰 선택하는 게 맞아 보여 공유를 위해 정리함. 
사실 개인 생각을 적은것이고 개발 방법에 정답이 있는 것이 아니라서 상황에 맞춰 방법을 사용하면 될 것 같음.


결론부터 말하자면
- 소규모 팀에서의 개발이라면 같은 git repository를 clone후  branch를 사용하여 협업하는 것을 추천
- 같은 팀이 아니거나 새로운 feature 개발을 위해서는 fork 된 git repository를 사용하여 협업하는 것을 추천.



사실 정해진 답은 아니라서 상황에 따라 적절한 방법을 사용하면 되겠지만 모든 코드 반영은 pull request를 통해 리뷰 후 반영하는 것은 당연!

특정 branch에 대해서 pull request를 강제하는 방법은 다음 가이드 참고

Enabling required reviews for pull requests  


그리고 fork는
 . Github의 기능으로 git repository를 통째로(branch 포함) 복사함.
 . 개발자는 forked git repository에 patch 생성 후 원 gir repository로 pull request를 함.
 . Fork 방법
   : 위 가이드에서 fork에 대해 원 project에 영향을 미치지 않고 마음껏 수정할 수 있도록 repository 복사한다("A fork is a copy of a repository. Forking a repository allows you to freely experiment with changes without affecting the original project. Mac") 라고 명시함.
 . 원 respository가 삭제되어도 fork 된 repository는 삭제되지 않음.




그래서 협업 시 fork를 사용하기 적합한 경우는 다음과 같은 상황이 아닌가 생각된다.
- Opensource Contribution과 같이 팀원이 아닌 (repository에 push할 권한이 없는) 상황에서 patch를 기여하거나
- 새로운 feature 개발을 위해 원 repository 업데이트로부터 영향을 최소하 하고자 할 경우


그리고 아래는 fork-centric 개발과 branch-centric 개발에 대한 의견인데 다소 fork-centric 개발에 대해 부정적이지만 참고해도 좋을 듯하다.

THE DEFINITIVE GUIDE TO FORKS AND BRANCHES IN GIT  





2020년 7월 7일 화요일

C++ 최적화 책, 7장 문장 최적화

C++ 최적화 책을 보게 되었고 읽으며 기억해야 할 내용을 간략히 정리 함.
C++ 언어를 사용하시거나 더 자세한 내용을 보시려면 아래 책을 꼭 읽어보세요. 오랜만에 읽는 유익한 C++ 책이네요.

C++ 최적화 최고 성능을 구현하는 10가지 검증된 기법
커트 건서로스 저/옥찬호 역/박수현 감수 | 한빛미디어

C++ 최적화 책, 4장 문자열 최적화
http://charlie0301.blogspot.com/2020/05/c-4.html

C++ 최적화 책, 6장 동적 할당 변수 최적화


반복문에서 코드 제거하기


- 반복문에서 종료값을 캐싱하라

char s[] = "This string is toooooooo long ";

for(size_t i=0; i< strlens(s);++i)
    if(s[i] == ' ')
        s[i] = '*';

위의 경우 문자열을 탐색하며 개수를 세는 strlen() 때문에 O(n) - O(n^2)의 비용이다.
이런 경우 값을 저장해 두고 사용하면 된다.

for(size_t i=0, len=strlen(s); i< len;++i)
    if(s[i] == ' ')
        s[i] = '*';

- 효율적인 반복문을 사용하라. 
다른 책에서도 do while()을 사용하는 것을 추천하던데 이유가 제대로 설명되어 있지 않았는데 이 책에서는 아래와 같이 설명

for(초기화 표현식; 조건식; 증감문) 반복해서 실행할 코드
는 다음과 같이 컴파일 된다고 함
        초기화 표현식;
L1: if (!조건식) goto L2;
        반복해서 실행할 코드;
        증감문;
        goto L1;
L2:

for 문은  조건식 결과가 false일 경우 두번 점프해야 함.
그에 반해 do while문은 다음과 같다.

do 반복해서 실행할 코드 while (조건식);
은 다음과 같이 컴파일 된다.
L1: 반복해서 실행할 코드
       if(조건식) goto L1;


- 반복문에서 불변 코드를 제거하라.
 : 반복문 진행 중에 변경되지 않는 코드는 컴파일러가 자동으로 반복문 바깥으로 옮기지만 함수가 복잡하거나 함수의 본문이 다른 컴파일 단위에 있다면 컴파일러가 자동으로 처리할 수 없으니 직접 확인 해서 반복문 바깥으로 옮겨라.


- 반복문에서 불필요한 함수 호출을 제거하라.
 : 반복문내의 제공하는 값이 항상 동일하거나 반복문과 상관 없는 함수는 제거하거나 외부로 옮기는 것이 맞다.


 순수함수(pure function)

아래 wikipedia를 보면 간단하게 말하면 함수에서 local static 변수, non-local 변수, parameter를 변경하거나 I/O 기기의 값을 참조하지 않아 같은 입력에 대해서 같은 출력을 제공하는 함수를 말한다. 

In computer programming, a pure function is a function that has the following properties:[1][2]

  1. Its return value is the same for the same arguments (no variation with local static variablesnon-local variables, mutable reference arguments or input streams from I/O devices).
  2. Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).


아래 글을 참고하면 pure function의 경우 컴파일러가 최적화 대상으로 인식하여 최적화 하게 된다.





- 반복문에서 숨겨진 함수 호출을 제거하세요.
 : 일반적으로 함수의 이름을 쓰고 괄호로 인수를 적어 호출하는 함수 외 클래스 타입 변수를 취급할 경우 다음 함수들이 호출 될 수 있다.
  . 클래스 인스턴스의 선언 (생성자를 호출)
  . 클래스 인스턴스의 초기화 (생성자를 호출)
  . 클래스 인스턴스의 대입 (대입 연산자를 호출)
  . 클래스 인스턴스를 포함하는 산술 표현식 (연산자 멤버 함수를 호출)
  . 범위를 빠져나갈 때 (범위에서 선언된 클래스 인스턴스의 소멸자를 호출)
  . 함수 인수 (각 인수 표현식은 형식 인수로 복사 생성됨)
  . 클래스 인스턴스의 함수 반환 (아마도 복사 생성자를 두 번 호출)
  . 표준 라이브러리 컨테이너에 항목을 삽입 (항목이 이동 또는 복사 생성됨)
  . 벡터에 항목을 삽입 (벡터가 재할당될 경우 모든 항목이 이동 또는 복사 생성됨)
 : 클래스의 불필요한 생성자, 연산자를 제거하거나 반복문 내에서 클래스 인스턴스 생성, 삭제, 복사를 최소한으로 하는 것이 방법

- 반복문을 함수 안에 넣어 호출 오버헤드를 줄이세요.
 : 문자열, 배열, 다른 자료구조를 반복하며 함수를 호출한다면 반복문 뒤집기(loop inversion) 기법을 사용하여 함수 호출 횟수를 줄여 성능 향상이 가능하다.



loop inversion

이것이 저자가 말하는 기법인지 모르겠으나 설명에 의하면 
while loop을 do..while loop을 가지는 if문으로 변경하는 방법으로
적절히 사용되면 cpu instruction pipelining에 의해 성능을 향상할 수 있다고 함.

위 링크의 어셈블리를 예를 확인하면  while loop 보다 if do..while loop이 불필요한 실행이 감소된 것을 확인할 수 있다.

https://en.m.wikipedia.org/wiki/Loop_optimization 에서도 하나의 기법으로 소개하고 있음.


* 책의 저자는 여러 저수준 최적화 기법이 있지만 생각만큼 효과가 없을 수 있다고 말하며 오히려 이미 C++ 컴파일러가 훌륭히 처리하고 있다고 함.


함수에서 코드 제거하기

함수가 호출 되면
컴퓨터는 현재 실행 중인 코드의 위치를 저장하고,
함수 본문으로 실행 흐름을 바꾼 다음,
함수 호출이 끝나고 이전에 실행하던 명령어의 다음 위치로 복귀하는 방법으로
실행 흐름에 함수 본문을 효율적으로 집어 넣음.

1. 실행 코드는 함수의 인수와 지역 변수를 저장하기 위해 호출 스택에 새 프레임을 삽입
2. 각 인수 표현식을 계산한 뒤 스택 프레임에 복사
3. 현재 실행 주소를 복사해서 스택 프레임에 반환 주소로 넣음.
4. 실행 코드는 실행 주소를 (함수를 호출한 후의 다음 문장 대신) 함수 본문의 첫 번째 문장으로 갱신
5. 함수 본문에 있는 명령어들을 실행
6. 스택 프레임에 저장되어 있는 반환 주소를 명령어 주소에 복사. 그리고 함수를 호출한 후의 문장으로 제어권을 넘김
7. 호출 스택에서 스택 프레임을 삭제

함수 호출의 기본 비용
- 함수 인수
 : 인수 표현식을 계산하는 비용, 인수값을 메모리 스택에 복사하는 비용 필요
 : 인수 몇개는 레지스터를 통해 전달 가능하지만 많으면 스택으로 전달함.

- 멤버 함수 호출(vs 함수 호출)
 : 멤버 함수를 호출하는 모든 코드에는 자신을 가리키는 this 포인터가 숨겨져 있다.

가상함수의 비용
 - 가상 멤버 함수가 있는 클래스의 각 인스턴스는 vtable이라는 테이블을 가리키는 이름 없는 포인터를 포함
 : vtable은 가상 함수들의 시그니처와 연관된 본문을 가리킴
 : vtable 포인터는 역참조 비용을 줄이기 위해 클래스 인스턴스의 첫번째 필드로 만듬.
 : 가상 함수를 호출하는 코드는 vtable을 가리키는 포인터를 얻고자 클래스 인스턴스를 가리키는 포인터를 역참조함. 즉 호출 시 비 순차적인 메모리를 추가로 두번 불러와야 함.

파생 클래스에서의 멤버 함수 호출의 비용도 상당하다.

함수를 가리키는 포인터의 비용
- 함수 포인터 (함수와 정적 멤버 함수를 가리키는 포인터)
 : 코드는 함수의 실행 주소를 얻기 위해 포인터를 역참조함. 컴파일러는 인라인할 수 없음.
- 멤버 함수 포인터
 : 함수 시그니처와 클래스를 모두 식별해야 하며 최악의 경우에 해당하는 성능을 갖는다고 가정해도 무리는 아님

- 인수가 없는 C 스타일의 void 함수는 호출 비용이 적음.
 : 함수를 인라인할 수 있다면 비용이 들지 않고 메모리 접근과 실행 비용이 전부
- 가상 다중 상속 클래스를 포함하지만 가상 함수가 없는 기본 클래스에서 파생된 클래스의 가상 함수를 호출하는 것이 최악의 경우
 : 코드는 클래스 인스턴스 포인터에 더할 오프셋을 결정하기 위해 클래스 인스턴스 테이블을 역참조
 : 가상 다중 상속된 함수의 인스턴스 포인터를 형성하고 vtable을 얻기 위해 인스턴스를 역참조한 뒤 함수의 실행 주소를 얻기 위해 vtable을 인덱싱

- 간단한 함수는 인라인으로 선언
- 함수를 처음 사용하기 전에 정의
 : 정의가 호출 전에 있다면 컴파일러가 함수를 호출하는 코드를 최적화 가능
- 사용하지 않는 다형성을 제거
 : 런타임 다형성을 구현하기 위해 가상 멤버 함수를 자주 사용함.

- 사용하지 않는 인터페이스를 버려라
- 템플릿으로 컴파일 타임에 구현을 선택하라
- PIMPL 관용구를 사용하는 코드를 제거하라
- 멤버 함수 대신 정적 멤버 함수를 사용하라
- 가상 소멸자를 기본 클래스로 옮겨라



표현식 최적화

- 표현식을 단순하게 만들어라
y = a*x*x*x + b*x*x +c*x + d;
=> y = (((a*x + b)*x) + c)*x + d;
로 바꿔 단순화 할 수 있다.

- 상수를 함께 모아라.
seconds = 24 * 60 * 60 * days;
seconds = days * (24 * 60 * 60);
이런 문장을 컴파일러에서는
seconds = 86400 * days;
바꿀 수 있지만 
seconds = 24 * days * 60 * 60;
인 경우 컴파일러는 곱셈을 런타임에 계산해야 한다고 함.

- 비용이 적은 연산자를 사용하라.
- 부동 소수점 연산 대신 정수 연산을 사용하라.
- double이 float보다 빠를 수 있다.
- 반복 계산을 닫힌 형태라 바꿔라


제어 흐름 최적화

- if-elseif-else 대신 switch를 사용하라.
 : if-else if-else 문은 선형 제어 흐름이고 O(n)번 비교함.
 : switch문은 색인 작업을 한 번 수행하고 테이블에 있는 주소로 점프함.
  . 비교하는데 O(1)이지만 색인에 사용되는 상수의 간격이 크다면 컴파일러는 상수를 정렬하여 이진 검색을 수행하는 코드를 생성함. 그래도 O(log2n)이다.

- swtich나 if 대신 가상 함수를 사용하라.
- 비용이 들지 않는 예외 처리를 사용하라.
  : 예외 사양을 사용하지 마라.
  : 예외 사양에는 개발자가 호출한 함수 라이브러리의 함수에서 어떤 예외를 던질 수 있는지 알아내기가 어렵다.
  : 예외 사양은 성능에 부정적인 영향을 미친다.
  : C++11에서는 더이상 예외 사양이 사용되지 않고 noexcept라는 새로운 예외 사양이 도입됨.
   . 함수를 noexcept로 선언하면 함수가 예외를 던질 수 없다고 알려주는 것임.
   . 컴파일러가 이동 문법을 구현하기 위해 특정 이동 생성자와 이동 대입 연산자를 noexcept로 선언해야 한다고 요구함. 이는 예외 안정성 보장 보다 이동 문법이 중요하다는 선언과 같음.


 
noexcept 및 C++에서의 예외처리 기본 지침

C++11부터 정의된 keyword로 함수에서 예외를 throw 할 수 있는지를 지정할 때 사용됨.


noexcept(true)의 별칭인 throw()를 제외하고는 C++17에서 모두 제거됨.
각각은 아래와 같은 의미를 가짐 

예외 사양의미
noexcept
noexcept(true)
throw()
이 함수는 예외를 throw하지 않습니다. /Std: c + + 14 모드 (기본값)에서는 noexcept 및 noexcept(true)가 동일 합니다. noexcept 또는 noexcept(true)선언 된 함수에서 예외가 throw 되는 경우 std:: terminate 가 호출 됩니다. /Std: c + + 14 모드에서 throw()로 선언 된 함수에서 예외가 throw 되는 경우 결과는 정의 되지 않은 동작입니다. 특정 함수가 호출 되지 않습니다. 이는 컴파일러에서 std::를 호출 하는 데 필요한 c + + 14 표준과의 차이입니다.
Visual Studio 2017 버전 15.5 이상/std: c + + 17 모드에서 noexceptnoexcept(true)및 throw() 모두 동일 합니다. /Std: c + + 17 모드에서 throw()는 noexcept(true)에 대 한 별칭입니다. /Std: c + + 17 모드에서 이러한 사양 중 하나를 사용 하 여 선언 된 함수에서 예외가 throw 되는 경우 std:: Terminate 는 c + + 17 표준에 필요한 대로 호출 됩니다.
noexcept(false)
throw(...)
사양 없음
함수는 모든 형식의 예외를 throw 할 수 있습니다.
throw(type)(C + + 14 및 이전) 함수는 type형식의 예외를 throw 할 수 있습니다. 컴파일러는 구문을 허용 하지만 noexcept(false)로 해석 합니다. /Std: c + + 17 모드에서 컴파일러는 경고 될 때 c5043를 발생 시킵니다.



기본 지침

강력한 오류 처리는 모든 프로그래밍 언어에서 어렵습니다. 예외가 좋은 오류 처리를 지원하는 여러 기능을 제공하기는 하지만 사용자를 대신해 모든 작업을 수행할 수는 없습니다. 예외 메커니즘의 혜택을 누리려면 코드를 디자인할 때 예뢰를 염두에 둡니다.

  • 절대 발생하지 않아야 하는 오류를 확인하려면 어설션을 사용합니다. 예외를 사용하여 발생할 수 있는 오류, 예를 들어 공용 함수의 매개 변수에 대한 입력 유효성 검증 오류를 점검하세요. 자세한 내용은 예외 및 어설션이라는 섹션을 참조 하십시오.

  • 오류를 처리하는 코드가 하나 이상의 개입 함수 호출로 오류를 감지하는 코드와 분리될 수 있는 경우 예외를 사용합니다. 오류를 처리하는 코드가 오류를 감지하는 코드에 밀접하게 연결되어 있으면 성능이 중요한 루프에서 오류 코드를 대신 사용할지 고려합니다.

  • 예외를 throw 하거나 전파 하는 모든 함수에 대해, 강력한 보장, 기본 보장 또는 nothrow (noexcept) 보장의 세 가지 예외 보장 중 하나를 제공 합니다. 자세한 내용은 방법: 예외 안전성을 위한 디자인을 참조 하세요.

  • 값에 따라 예외를 발생(throw)시키고 참조로 포착(catch)합니다. 처리할 수 없는 것은 포착(catch)하지 않도록 주의합니다.

  • C++11에서 더이상 사용되지 않는 예외 사양을 사용하지 마세요. 자세한 내용은 예외 사양 및 noexcept섹션을 참조 하세요.

  • 가능하다면 표준 라이브러리 예외 형식을 사용합니다. 예외 클래스 계층 구조에서 사용자 지정 예외 형식을 파생 시킵니다.

  • 소멸자 또는 메모리 할당 해제 함수에서 예외가 빠져나오는 것을 허용하지 마세요.






2020년 6월 23일 화요일

C# 객체에서의 unmanged/manged resource 처리 관련 (Dispose pattern)

예전에 C# RAII(Resource Acquisition Is Initialization) 게시물에서 적기는 했었는데

C# garbage collection의 정책상 object instance가 scope을 벗어나거나 더이상 사용되지 않더라도 바로 destory되지 않으므로
- 명시적인 resource 해제를 위한 IDisposable 인터페이스를 구현하고
- using문을 사용하여 object가 관리(생성,사용,삭제)되도록 해야 한다.


아래는 IDisposable interface에 대한 내용인데 Dispose pattern을 소개하고 있다. 

IDisposable interface

Implement a Dispose method


Dispose pattern 예제 코드이다. 영어 주석을 간략히 한글로 달았음.

    public class MyResource: IDisposable
    {
        // 외부에서 전달받은 unmanaged 리소스
        private IntPtr handle;
        // 내부에서 생성하는 managed 리소스
        private Component component = new Component();
        // Dispose가 호출 되었음을 알려주는 flag
        private bool disposed = false;

        public MyResource(IntPtr handle)
        {
            this.handle = handle;
        }

        // 리소스를 해제하기 위한 method
        // 하위 class에서 재정의 하지 못하도록 virtual이 아님
        public void Dispose()
        {
            Dispose(true);
            // object가 위 Dispose메서드 호출로 정리되지만 
            // object가 finalization queue에 포함되어 종료자/finalizer가 호출되지 않도록 
            // 아래 명령어를 호출한다.
            GC.SuppressFinalize(this);
        }

        // 아래 함수가 호출되는 경우는 두가지인데
        // 첫번째는(disposing == true) 사용자의 코드를 통해 호출되는 경우, 이 때는 managed/unmanaged 리소스를 해제해야 한다.
        // 두번째는(disposing == false) finalizer가 호출한 상황으로 다른 obejct가 어떤상황인지 모르므로 unmanaged resource만 해제해야 한다.
        protected virtual void Dispose(bool disposing)
        {
            if(!this.disposed)
            {
                if(disposing)
                {
                    // 첫번째 경우 managed 리소스 해제
                    component.Dispose();
                }
                // 첫번째, 두번째 경우 unmanaged 리소스 해제
                CloseHandle(handle);
                handle = IntPtr.Zero;
                // dispose가 호출되었음을 기록
                disposed = true;
            }
        }

        // unmanaged 리소스를 해제하기 위한 interop
        [System.Runtime.InteropServices.DllImport("Kernel32")]
        private extern static Boolean CloseHandle(IntPtr handle);

        // finalization을 위한 종료자/finalizer(소멸자/destructor)
        // Dispose()가 호출되면 아래 코드는 호출되지 않는다.
        ~MyResource()
        {
            Dispose(false);
        }
    }


위 Dispose pattern을 따라 구현하면
- Dispose()를 명시적으로 호출하여 자원을 해제하게 하거나
  사용자가 Dispose() 호출을 잊어버렸을 때 자원을 해제할 수 있게 한다.
- Dispose()를 여러번 호출해도 문제가 없음
- 종료자/finalizer 가 호출 될 때 object가 Dispose가 된 상태라도 문제가 없다.

하지만 이 pattern이 자원 해제와 메모리 해제의 결합도를 가지게하여 종료자 스레드의 부담을 증가한다는 점이 있다고 한다.

함께 위 패턴을 사용함에 있어 Dispose 구현을 사용하게 하려면 Using 문이나 Try/Finally 블록을 사용하면 된다. 어차피 Using이 Try/Finally 생성처럼 동작한다고 함.

Using block has three parts: acquisition, usage, and disposal.

  • Acquisition means creating a variable and initializing it to refer to the system resource. The Using statement can acquire one or more resources, or you can acquire exactly one resource before entering the block and supply it to the Using statement. If you supply resourceexpression, you must acquire the resource before passing control to the Using statement.

  • Usage means accessing the resources and performing actions with them. The statements between Using and End Using represent the usage of the resources.

  • Disposal means calling the Dispose method on the object in resourcename. This allows the object to cleanly terminate its resources. The End Using statement disposes of the resources under the Using block's control.


만약 부모 클래스를 상속하는 자식 클래스가 있다면 자식 클래스에서는 다음과 같은 패턴을 구현해야 한다고 함.

Subclasses should implement the disposable pattern as follows:

  • They must override Dispose(Boolean) and call the base class Dispose(Boolean) implementation.

  • They can provide a finalizer if needed. The finalizer must call Dispose(false).


아래 예제에서는 종료자/finalizer는 구현하지 않는 자식클래스 예제임.

class DerivedClass : BaseClass
{
   bool disposed = false;
   SafeHandle handle = new SafeFileHandle(IntPtr.Zero, true);

   protected override void Dispose(bool disposing)
   {
      if (disposed)
         return;

      if (disposing) {
         handle.Dispose();
         // 다른 managed 리소스를 해제
      }

      // unmanaged 리소스를 해제

      disposed = true;
      // 부모 클래스의 Dispose(bool)를 호출
      base.Dispose(disposing);
   }
}


그리고 아래 책을 보면

C# 6.0 완벽 가이드 깊고 넓게 알려주는 레퍼런스 북 [ 전2권 ]
조셉 앨버허리, 벤 앨버허리 공저 / 류광 역 | 인사이트(insight) 

종료자/finalizer에 대한 언급이 있다.

객체에 종료자/finalizer가 있다면 인스턴스의 메모리 해제 시 종료자/finalizer가 호출된다. 
. 종료자/finalizer가 없다면 객체는 garbage collection 시 바로 삭제되지만
  종료자/finalizer가 있다면 별도의 대기열에 저장하여 관리하며 종료자/finalizer 호출 전까지 객체가 유지된다. 
. 종료자 때문에 메모리할당과 garbage collection이 느려진다. (종료자 객체들의 관리 및 처리)
. 종료자는 객체와 객체가 참조하는 객체의 수명을 필요 이상으로 늘린다.
. 종료자의 호출 순서를 예측할 수 없고 시점을 제어할 수 없다.
. 종료자안에서 코드 실행이 차단되면 다른 객체의 종료자가 호출되지 못한다.
. app이 제대로 종료되지 않는다면 종료자들이 호출되지 않을 수 있다.

그래서 더더욱 resource 해제를 위해서라면 위 Dispose pattern을 구현하는것이 맞겠다.


마지막으로 Garbage collection에 대해 설명하는 공식 문서

2020년 6월 10일 수요일

C++ 최적화 책, 6장 동적 할당 변수 최적화

C++ 최적화 책을 보게 되었고 읽으며 기억해야 할 내용을 간략히 정리 함.
C++ 언어를 사용하시거나 더 자세한 내용을 보시려면 아래 책을 꼭 읽어보세요. 오랜만에 읽는 유익한 C++ 책이네요.

C++ 최적화 최고 성능을 구현하는 10가지 검증된 기법
커트 건서로스 저/옥찬호 역/박수현 감수 | 한빛미디어

C++ 최적화 책, 4장 문자열 최적화
http://charlie0301.blogspot.com/2020/05/c-4.html


C++ 변수

- C++ 변수는 고정된 레이아웃을 가지고 컴파일 타임에 크기가 결정됨
- 변수는 저장 기간(수명)을 가짐
- 정적 저장 기간
 . 컴파일러가 예약해둔 메모리에 상주
 . 메모리 주소는 컴파일 타임에 결정
 . 전역 정적 변수는 main()함수 진입 전에 생성, main()함수를 떠난뒤 파괴
 . 함수에서 선언한 정적 변수는 함수에 처음 진입하기 전에 생성
 
- 스레드 지역 저장 기간
 . C++ 11 이후 thread-local storage(TLS) 변수를 선언 가능
 . 스레드 진입 시 생성, 스레드가 끝나면 파괴
 . thread_local 키워드로 선언된 변수는 스레드 지역 저장 기간을 가짐

- 자동 저장 기간
 . 함수 호출 스텍에서 컴파일러가 예약해둔 메모리 상주
 . 컴파일 타임에 메모리 크기 결정
 . 함수 호출 스택 포인터에서 고정된 오프셋 위치를 가짐
 . 코드 블록 {} 안에 존재하므로 {} 진입 시 생성, 빠져나오면 파괴

- 동적 저장 기간
 . 실행 중인 프로그램에서 요청한 메모리에 상주
 . 프로그램에서 new로 명시적으로 저장 공간을 요청하고 delete로 파괴
 . 메모리 관리자가 메모리 관리
 . 자동 변수와 마찬가지로 주소는 런타임에 결정


- 변수의 소유권( 변수의 생성, 파괴를 결정하는 주체를 설명하기 위한 개념인듯)
 : 전역 소유권
  . 프로그램이 가지며 main()함수 진입 전 생성되고 main()함수를 떠난 뒤 파괴 됨.
 : 유효 범위가 지정된 소유권 
  . 중괄호로 둘러 싸인 코드블록의 유효 범위가 소유권을 가짐. 
  . main() 함수의 {} 도 해당 되며 main()함수에서 선언된 자동 변수는 정적 변수와 동일한 수명을 가짐
 : 멤버 소유권 
  . 클래스 인스턴스가 가짐. 
  . 클래스 인스턴스가 생성될 때 생성되며 인스턴스가 파괴될 때 파괴됨.
 : 동적변수의 소유권 
  . 정해져 있지 않고 프로그램에서 명시적으로 관리해야 함. 
  . new로 만들어진 포인터를 프로그램이 관리하고 더이상 사용하지 않는 경우 delete로 제거해야 함. (포인터 날로 쓰는게 가장 싫었음. 가능하면 동적 변수는 스마트 포인터로 관리하는게 맞는 방법임.)


- C++11의 nullptr이라는 특정한 값은 유효한 메모리를 가지키지 않는다. 
  : 0을 nullptr로 변환할 수 있지만 0 != nullptr 이다.



POCO(Plain Old C++ 개체)에 대한 포인터를 캡슐화하는 데 가장 먼저 스마트 포인터를 사용합니다.

  • unique_ptr
    기본 포인터로 한 명의 소유자만 허용합니다. shared_ptr이 필요하다는 점을 확실히 알 경우 POCO의 기본 선택으로 사용합니다. 새 소유자로 이동할 수 있지만 복사하거나 공유할 수 없습니다. 사용하지 않는 auto_ptr을 대체합니다.boost::scoped_ptr과 비교합니다. unique_ptr작고 효율적입니다. 크기는 하나의 포인터이며 C++ 표준 라이브러리 컬렉션에서 빠른 삽입 및 검색을 위한 rvalue 참조를 지원합니다. 헤더 파일: <memory>. 자세한 내용은 unique_ptr 인스턴스 및 unique_ptr 클래스를 만드는 방법( 사용 방법)을 참조하십시오.

  • shared_ptr
    참조 횟수가 계산되는 스마트 포인터입니다. 원시 포인터 하나를 여러 소유자에게 할당하려고 할 경우 사용합니다(예: 컨테이너에서 포인터 복사본을 반환할 때 원본을 유지하고 싶을 경우). 원시 포인터는 모든 shared_ptr 소유자가 범위를 벗어나거나 소유권을 포기할 때까지 삭제되지 않습니다. 크기는 2개의 포인터입니다. 하나는 개체용이고, 다른 하나는 참조 횟수가 포함된 공유 제어 블록용입니다.헤더 파일: <memory>. 자세한 내용은 인스턴스 및 shared_ptr 클래스를shared_ptr 만드는 방법: 인스턴스 만들기 및 사용 방법을 참조하십시오.

  • weak_ptr
    shared_ptr과 함께 사용할 수 있는 특별한 경우의 스마트 포인터입니다.weak_ptr은 하나 이상의 shared_ptr 인스턴스가 소유하는 개체에 대한 액세스를 제공하지만, 참조 수 계산에 참가하지 않습니다. 개체를 관찰하는 동시에 해당 개체를 활성 상태로 유지하지 않으려는 경우 사용합니다. shared_ptr 인스턴스 사이의 순환 참조를 차단하기 위해 필요한 경우도 있습니다. 헤더 파일: <memory>. 자세한 내용은 weak_ptr 인스턴스 및 weak_ptr 클래스를 만드는 방법: 인스턴스 만들기 및 사용 방법을 참조하십시오.



스마트 포인터의 삭제 조건
- break, continue를 만났을 때, 함수에서 값을 리턴할 때, 예외가 발생하는 등 선언문을 둘러싸고 이쓴 범위를 빠져 나갈 때 소유한 동적 변수 삭제
- 클래스 멤버로 선언된 스마트 포인터는 클래스 인스턴스가 파괴될 때 소유한 동적 변수를 삭제함.
- 스레드 지역 저장 기간으로 선언된 스마트 포인터 인스턴스는 스레드가 정상 종료될 때 소유한 동적 변수를 삭제
 : 운영체제가 스레드를 종료시킨 경우 일반적으로 삭제되지 않는 다고 함.
- 정적 저장 기간으로 선언된 스마트 포인터의 인스턴스는 프로그램이 종료될 때 소유한 동적 변수 삭제

- 동적 변수를 한 소유자가 관리하는 경우 
 : std::unique_ptr 사용 추천. 비용 패널티도 거의 없음.

스마트 포인터는 메모리 및 성능 관점에서 최대한 효율적으로 사용할 수 있도록 설계되었습니다. 예를 들어, unique_ptr의 유일한 데이터 멤버는 캡슐화된 포인터입니다. 즉, unique_ptr은 해당 포인터와 정확히 동일한 크기(4바이트 또는 8바이트)입니다. 오버로드된 스마트 포인터 * 및 -> 연산자(s)를 사용하여 캡슐화된 포인터에 액세스하는 것은 원시 포인터에 직접 액세스하는 것보다 훨씬 느리지 않습니다.


- 동적 변수를 여려명이 공유하는 경우, 두개 이상의 포인터가 동적변수를 관리하는 경우 접근 시 유효성을 확인하기 어렵고 삭제 같은 관리가 어려움. 
 : std::shared_ptr를 사용하는 것을 추천
 : 단 shared_ptr에 대입할 때 new 표현식에서 리턴되는 것을 바로 넣고 std::make_shared를 사용하는 것을 추천

- 동적 변수는 런타임 비용이 있음.
- 동적 변수의 메모리를 할당하기 위해서는 수천번의 메모리 접근이 필요할 때도 있음.
 : 동적 변수 생성을 위해 메모리 할당 함수는 빈 메모리 블럭을 찾음.
  . 빈 메모리 블록을 찾으면 블록을 제거한 뒤 제공
  . 필요한 것보다 큰 블록을 찾으면 분할한 뒤 일부를 반환
 : 이용 가능한 메모리 블록이 없다면 할당 함수는 큰 메모리 블럭을 추가로 얻기 위해 가용 메모리 시스템 풀에서 운영체제 커널을 고비용으로 호출
  . 커널에서 사용하는 메모리는 RAM에 캐시되어 있을 수도 없을 수도 있음. 캐시되어 있지 않다면 지연 될 수 있음
 : 빈 메모리 블록의 컬렉션은 프로그램의 모든 스레드가 공유하는 자원이며 메모리 블록 변경 사항은 스레드 세이프(thread-safe) 함
  . 할당, 해제 함수를 자주 호출하면 하나 외 다른 스레드들이 대기하게 됨
 : 동적 변수의 할당된 메모리를 해제할 때 반환된 블럭을 빈 메모리 컬렉션에 넣게 되지만 복잡함.
  . 할당, 해제로 블록들의 크기가 작아지는 것을 방지하기 위해 해제된 메모리 블록을 인접한 메모리 블록과 합치려고 시도함. 


동적 변수 사용 줄이기

- 클래스 인스턴스를 정적으로 만들어라
  ex) YourClass yourInstance("hahaha",111);

- 클래스 멤버 변수를 정적으로 만들어라
 : 클래스 멤버변수가 클래스 인스턴스라면 클래스 생성 시 멤버변수도 정적으로 생성되어 할당하는 비용을 줄일 수 있음.
 : 클래스 생성 시 멤버 변수를 생성하기 위한 자원이 없어 동적으로 생성해야 하는 경우 두 단계초기화(two-part initialization)을 사용하라.

- 배열의 크기가 정해져 있다면 std::vector 대신 std::array를 사용하라.
 : std::array는 복사 생성할 수 있고 operator[]로 임의 접근 반복자와 첨자 지정을 제공함.

- 저장공간이 커지면서 발생하는 재할당 비용을 줄이기 위해 스택에 큰 버퍼를 만들어라

- 정적으로 초기화된 데이터가 연결 자료 구조라면 정적으로 구성하라.

- 이진트리를 배열로 만들어라

- std::deque 대신 원형 버퍼(http://bit.ly/b-buffer)를 사용하라.

- 변수 할당 비용을 줄이기 위해 new 대신 std::make_shared를 사용하라.
  ex) std::shared_ptr p = std::make_shared("hahah",111);
        auto p = std::make_shared("hahah",111);

- 동적 변수를 사용한다면 std::unique_ptr, std::shared_ptr을 사용하라.


동적 변수의 재할당 줄이기

- 동적 변수를 미리 할당하여 재할당을 방지하라.
 : 메모리할당 비용을 줄이기 위해 std::string, std::vector의 reserve(size_t n) 같은 함수로 적당히 공간을 확보하라.
- 반복문 바깥에서 동적 변수를 만들어라
 : 반복문내의 변수는 할당/해제를 반복하므로 외부에 두고 clear()같은 함수를 사용하여 초기화 후 사용하라.


불필요한 복사 제거하기

- 클래스의 대입 시(ClassA = ClassB)의 대입 연산자(Assignment operator)가 호출 됨. 이 때 클래스 멤버에 따라서 대입하는 비용이 상당히 커질 수 있음.
- 선언과 동시에 초기화 하는 문장(Foo a = b)의 경우 Foo가 클래스라면 복사생성자(Copy constructor)가 호출 됨. 
 : 대입 연산자와 복사 생성자는 하는일이 거의 동일하고 비용이 클 수 있어 최적화 시 다음상황에서 발생가능한지 확인해야 함.

- 초기화 (생성자 호출)
- 대입 (대입 연산자 호출)
- 함수 인수 (함수의 인자/형식 인수로 전달되면서 이동 생성이나 복사 생성됨)
- 함수 반환 (이동 생성자나 복사 생성자를 호출)
- 표준 라이브러리 컨테이너에 항목을 삽입 (항목은 이동 생성이나 복사 생성됨)
- 벡터에 항목을 삽입 (벡터가 재할당 될 경우 모든 항목은 이동 생성이나 복사 생성됨)

- 클래스 정의에서 원치 않는 복사 방지하기
 : 클래스 인스턴스를 복사하는 비용이 많거나 복사를 원하지 않는다면 복사를 금지할 수 있음.
 : 단 아래와 같이 복사를 금지한다면 표준 라이브러리 컨테이너 클래스 값으로 사용하지 못함.

// C++ 11 이전 방법
class BigClass {
private :
   BigClass(BigClass const&);
   BigClass& operator=(BigClass const&);
public : 
...
};

// C++11 이후
class BigClass {
public 
   BigClass(BigClass const&) = delete;
   BigClass& operator=(BigClass const&) = delete;
   ...
};


- 함수 호출에서 복사 제거하기, 
 : 함수 인자로 넘겨지는 클래스 인스턴스의 경우 레퍼런스로 받아라. 
 : 내부에서 수정이 필요 없으면 const &, 수정이 필요하면 &
 : 하지만 함수 내에서 레퍼런스값을 여러번 참조한다면 역참조(dereferencing)로 인한 비용이 더 커질 수 있음.

포인터나 레퍼런스에서 값을 확인할 때를 역참조라 표현


- 함수 반환에서 복사 제거하기
 : 함수에서 클래스 인스턴스를 리턴 시 반환값으로 복사 생성됨.
 : 하지만 C++ 컴파일러에서는 복사 생략(copy elision), 반환값 최적화(return value optimization, RVO) 방법을 사용하지만 구체적인 조건에서만 가능함.
   . 함수는 내부에서 생성된 객체를 반환해야 함.
   . 함수에서 선언했던 반환 타입과 똑같은 타입이어야 함.
   . 함수가 간단하고 제어 경로가 하나뿐이라면 컴파일러는 RVO를 수행할 가능성이 높음
 : 그 외 방법으로는 출력용 매개변수(out parameter)를 사용하여 값을 반환하는 것도 방법임.
  ex) vector scalar_product(std::vector const& v, int c) 
        =>  void scalar_product(std::vector const& v, int c, vector& result)
 

RVO(Return Value Optimization)
 Output (note that -fno-elide-constructors disables RVO in clang):
$ clang++ -std=c++11 main.cpp && ./a.out
c'tor
d'tor

$ clang++ -std=c++11 -fno-elide-constructors main.cpp && ./a.out
c'tor
move c'tor
d'tor
move c'tor
d'tor
d'tor
RVO 적용과 미적용의 차이를 확실히 알 수 있다.


- COW(Copy On Write) 구현하기
 : COW의 개념은 원본 객체와 복사한 객체 중 하나가 수정되기 전까지 객체가 동일하다는 것
    즉 초기에는 얕은 복사(shallow copy)를 하고 객체가 수정될 때까지 깊은 복사(deep copy)를 지연 시키는 것


COW(Copy On Write)
예시를 보여주고 있지만 주의점도 함께 설명하고 있다.

n a multi-threaded environemnt (which is most of them nowadays) CoW is frequently a huge performance hit rather than a gain. And with careful use of const references, it's not much of a performance gain even in a single threaded environment. 
Additionally, as other people have pointed out, CoW strings are really tricky to implement, and it's easy to make mistakes. That coupled with their poor performance in threading situations makes me really question their usefulness in general. This becomes even more true once you start using C++11 move construction and move assignment. 
답변의 내용이 생각보다 부정적인 반응임. 구현하기 까다롭고 성능 개선도 크지 않을 수 있다고 함.


이동 문법 구현하기
책의 내용을 참고하자. 어렵지만 이 책(https://www.oreilly.com/library/view/effective-modern-c/9781491908419/)도 참고하면 좋다.


평평한(flat) 자료구조
- 자료 구조 요소들이 인접한 저장 공간에 저장되었다면 자료구조가 평평하다고 표현함.
- 평평한 자료구조는 생성 시 포인터로 연결된 자료구조보다 메모리 관리자를 호출하는 횟수가 적음
- list, deque, map, unordered_map 자료구조는 동적 객체를 많이 만드는 반면 vector는 이보다 적게 만듬. 같은 big O 성능을 갖는다고 하더라도 평평한 자료구조가 가지는 장점이 많음.
- std::array, std::vector와 같은 평평한 자료구조는 list, map, unordered_map과 같은 노드 기반의 자료구조보다 메모리를 적게 차지함.
- 평평한 자료구조를 사용하면 스마트 포인터와 스마트 포인터가 가리키는 객체를 할당하는 런타임 비용을 없앨 수 있음.

2020년 6월 4일 목요일

C++ class 선언 시 소멸자, 복사 생성자(copy constructor), 이동 생성자(move constructor) 처리, The rule of three/five/zero

C++에서는 기본적인 내용인데 간과하기 쉬운게 class 생성시 member 변수의 형식에 따라서 생성자, 소멸자, 연산자를 처리해줘야 한다. 기본적인 생성자, 소멸자, 복사 생성자/할당 연산자, 이동 생성자/할당 연산자에 대해서는 아래 MSDN을 참고하면 자세하고 친절히 설명해준다.

- 생성자(constrctor), 소멸자(destructor)

- 복사 생성자(copy constructor), 복사 할당 연산자(copy assign operator)

- 이동 생성자(move constructor), 이동 할당 연산자(move assignment operator)


예를 들어 class member 변수가 pointer와 같이 복사, 이동 시 관리되지 않는 변수 형식이라면 class가 생성, 소멸, 복사, 이동 시 해당 member 변수가 정상적으로 할당, 삭제, 복사, 이동 될 수 있도록 해줘야 한다.

왜나면 C++에서는 특정 조건에 따라 컴파일러가 class의 생성자, 복사 생성자, 복사 할당 연산자, 소멸자를 자동으로 생성하게 되어 의도치 않게 member 변수가 복사, 이동 될 수 있음.

"C++ 얕은복사 vs 깊은복사" 검색어나 "C++ shallow copy vs deep copy" 검색어로 검색하면 관련 글들이 꽤 나온다.


그래서 C++ class 선언과 관련된 The rule of three/five/zero 같은 용어가 있음.

간단히 말하면 class 선언 시 member 변수의 처리에 따라 3개, 5개, 0개의 생성자, 연산자를 정의하라는 것이고 간략히 내용을 발췌하면 다음과 같음.

아래 Rule of three 코드에서 복사 시 제대로 관리되지 않는 char* 형의 cstring member 변수가 있다.
만약 복사 생성자를 정의하지 않아 컴파일러가 복사 생성자를 만든다면 해당 member 변수는 복사 시 얕은 복사(shallow copy)가 되어 member 변수의 pointer 주소만 복사되어 두 class instance는 동일한 메모리를 바라보게 된다. 프로그램 실행 중 한 instance가 소멸되면 다른 instance의 변수가 영향을 받는다.

그래서 아래 코드에서는 3개(복사 생성자(copy constructor), 복사 할당 연산자(copy assignment), 소멸자) member 함수를 정의 하여 깊은 복사(deep copy)를 하여 복사된 class instance가 복사한 class instance와 다른 저장공간의 member 변수를 가지도록 한다.

일반적으로 관리되지 않는 member 변수 특히나 char* 형식의 단순 문자열을 가지고 있는 경우 아래와 같이 정의하여 처리한다.

Rule of three

class rule_of_three
{
    char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block
 
    void init(const char* s)
    {
        std::size_t n = std::strlen(s) + 1;
        cstring = new char[n];
        std::memcpy(cstring, s, n); // populate
    }
 public:
    rule_of_three(const char* s = "") { init(s); }
 
    ~rule_of_three()
    {
        delete[] cstring;  // deallocate
    }
 
    rule_of_three(const rule_of_three& other) // copy constructor
    { 
        init(other.cstring);
    }
 
    rule_of_three& operator=(const rule_of_three& other) // copy assignment
    {
        if(this != &other) {
            delete[] cstring;  // deallocate
            init(other.cstring);
        }
        return *this;
    }
};


아래 코드에서는 동일한 상황이지만 class instance가 이동 생성자(move constructor), 이동 할당 연산자(move assignment operator)가 사용되는 상황을 처리하기 위해 5개(이동 생성자(move constructor), 이동 할당 연산자(move assignment operator), 복사 생성자(copy constructor), 복사 할당 연산자(copy assignment), 소멸자) member 함수를 정의하였음.

예제의 swap 함수를 보면 알겠지만 할당된 메모리양이 많은 char* member 변수를 가지고 있는 경우 class instance 이동 시 performance를 고려하고자 이동 생성자, 할당 연산자를 정의하는 경우가 있다고 함.

Rule of five

class rule_of_five
{
    char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block
 public:
    rule_of_five(const char* s = "")
    : cstring(nullptr)
    { 
        if (s) {
            std::size_t n = std::strlen(s) + 1;
            cstring = new char[n];      // allocate
            std::memcpy(cstring, s, n); // populate 
        } 
    }
 
    ~rule_of_five()
    {
        delete[] cstring;  // deallocate
    }
 
    rule_of_five(const rule_of_five& other) // copy constructor
    : rule_of_five(other.cstring)
    {}
 
    rule_of_five(rule_of_five&& other) noexcept // move constructor
    : cstring(std::exchange(other.cstring, nullptr))
    {}
 
    rule_of_five& operator=(const rule_of_five& other) // copy assignment
    {
         return *this = rule_of_five(other);
    }
 
    rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment
    {
        std::swap(cstring, other.cstring);
        return *this;
    }
 
// alternatively, replace both assignment operators with 
//  rule_of_five& operator=(rule_of_five other) noexcept
//  {
//      std::swap(cstring, other.cstring);
//      return *this;
//  }
};

class의 member 변수가 복사, 이동 시 관리되는 경우라면 컴파일러가 생성하는 default 생성자, 연산자를 사용하도록 지정한 예제 코드들이다. 처음 코드 처럼 아무것도 선언하지 않거나 두번째 코드 처럼 default로 지정해도 class 생성, 복사, 이동 시 처리되는 결과는 동일한다.

C++로 개발하고 파일 operation 처리와 같이 byte stream을 처리해야 하는 경우가 아닌 단순 문자열을 처리하는 상황, performance가 critical한 요소가 아니라면 std::string을 사용하는게 당연해 보인다.

Rule of zero

class rule_of_zero
{
    std::string cppstring;
 public:
    rule_of_zero(const std::string& arg) : cppstring(arg) {}
};

class base_of_five_defaults
{
 public:
    base_of_five_defaults(const base_of_five_defaults&) = default;
    base_of_five_defaults(base_of_five_defaults&&) = default;
    base_of_five_defaults& operator=(const base_of_five_defaults&) = default;
    base_of_five_defaults& operator=(base_of_five_defaults&&) = default;
    virtual ~base_of_five_defaults() = default;
};


하지만 컴파일러가 자동으로 생성하는 것을 고려한다면 class 선언이 다음 규칙을 따르도록 해야 한다. 괜히 어설프가 일부 생성자, 연산자를 정의하면 자동으로 생성되지 않을 수 있어 곤란해질 수 있다.


 따라서 단순 형식에는 편리하지만 복합 형식은 종종 하나 이상의 특수 멤버 함수 자체를 정의하므로 다른 특수 멤버 함수가 자동으로 생성되지 않도록 할 수 있습니다. 실제로는 다음과 같습니다.
  • 생성자가 명시적으로 선언된 경우 기본 생성자가 자동으로 생성되지 않습니다.

  • 가상 소멸자가 명시적으로 선언된 경우 기본 소멸자가 자동으로 생성되지 않습니다.

  • 이동 생성자 혹은 이동 할당 연산자가 명시적으로 선언된 경우 다음과 같습니다.

    • 복사 생성자가 자동으로 생성되지 않습니다.

    • 복사 할당 연산자가 자동으로 생성되지 않습니다.

  • 복사 생성자, 복사 할당 연산자, 이동 생성자, 이동 할당 연산자 또는 소멸자가 명시적으로 선언된 경우 다음과 같습니다.

    • 이동 생성자가 자동으로 생성되지 않습니다.

    • 이동 할당 연산자가 자동으로 생성되지 않습니다.

 참고

또한 C++ 11 표준은 다음 추가 규칙을 지정합니다.

  • 복사 생성자나 소멸자가 명시적으로 선언된 경우 복사 할당 연산자가 자동으로 생성되지 않습니다.
  • 복사 할당 연산자나 소멸자가 명시적으로 선언된 경우 복사 생성자가 자동으로 생성되지 않습니다.

두 경우 모두 Visual Studio에서는 필요한 함수가 암시적으로 자동 생성되며 경고를 생성하지 않습니다.


참고로 아래 rule 같이 하나를 정의할 거면 모두 정의하던지 delete 처리하라던지 조언 하기도 함.

C.21: If you define or =delete any default operation, define or =delete them all