배고픈 개발자 이야기

Modern C++의 장/단점 본문

언어/C언어, C++언어

Modern C++의 장/단점

이융희 2019. 9. 20. 18:30
728x90

1.스마트 포인터

Widget *getObject() => unique_ptr<Object> getObject();

일반 포인터를 활용하여 프로그래밍을 할 경우 Exception, Return또는 끝나는 시점에 직접 메모리 해제을 해야함

하지만 스마트 포인터는 Exception, Return, 끝나는 시점 모두 메모리를 자동으로 해제함

 

 

2.무브 시맨틱

vector<Object>를 할당할 때 return by value Object를 복사하는 비용이 매우 많이 발생할 수 있음

모던 C++에서 무브 시맨틱을 사용하여 Object가 쓸데없이 복사되는 일을 한번에 날릴 수 있게 되었음

 

Object가 아무리 'move-enabled'가 아닐지라도, vector 템플릿 자체는 'move-enabled'이므로

함수로부터 Object를 위한 임시 컨테이너를 return by value로 넘겨주는 것은 아무래도 이전보다

효율적으로 동작함, 또한 Object 클래스가 move-enabled라면 함수에서 return시 요구되는 오버헤드가 극적으로 감소하게 됩니다.

 

            // 복사 생성자:

        Widget(const Widget &rhs) : ptr(new char[rhs.size]), size(rhs.size) {

            std::memcpy(ptr, rhs.ptr, size);

        }

 

            // 무브 생성자:

        Widget(Widget &&rhs) noexcept : ptr(rhs.ptr), size(rhs.size) {

            rhs.ptr = nullptr; rhs.size = 0;

        }

 

            // 복사 할당 생성자:

        Widget& operator=(const Widget &rhs) {

            Widget tmp(rhs);

            swap(tmp);

            return *this;

        }

 

        void swap(Widget &rhs) noexcept {

            std::swap(size, rhs.size);

            std::swap(ptr, rhs.ptr);

        }

 

            // 무브 할당 생성자:

        Widget &operator=(Widget &&rhs) noexcept {

            Widget tmp(std::move(rhs));

            swap(tmp);

            return *this;

        }

 

복사 할당할 때 복사 생성자로 쓸데없이 생성하고 값을 swap

 

테스트 프로그램은 간단한 timer 클래스를 이용하여 vector<Widget> 50만개의 인스턴스를 담은 뒤

vector의 메모리 할당 한계에 도달한 상태에서 추가로 1번의 push_back()을 수행하였을 때의 소모된 시간측정결과

C++98 53.668, 모던 C++ 0.032

 

C++ 최적화 : 인라인 / 중복 객체 제거 / 코드 생략

C11, C14부터 using으로 별칭을 자유롭게 쓰며, auto와 결합된 템플릿까지 써짐

 

int Array[5];  Array[5] = 5; 이게 왜 실수 일까 - 컴파일러가 잡아냄

 

*this = ::std::move(from);

::std::move(value) // parameter

families_.push_back(std::move(family))

 

 

 

 

 

 

 

 

 

 

 

 

 

 

auto, lambda함수

 

C++ 11 이후로 auto 선언을 통한 형식 영역이 가능하게 되었다.

-명시적 형식 선언보다는 auto를 선호하라.

-auto를 잘 활용하면 타이핑의 양이 줄어들 뿐만 아니라, 형식을 직접 지정했을 때 겪게 되는

정확성 문제와 성능 문제도 방지 할 수 있다.

 

1) auto를 사용함으로써 초기값을 설정하지 않아서 발생하는 정확성 문제를 원천 봉쇄할 수 있다.

auto x2; // 오류! 초기치가 꼭 필요함

auto x3 = 0;

 

2) C++ 14 에서는 람다 표현식의 매개변수에도 auto를 적용할 수 있어서 더욱 후련해진다.

auto derefLess = [] (const auto& p1, const auto& p2) { return *p1 < *p2; };

-std::function으로 선언된 변수의 형식은 auto로 선언된 객체보다 메모리를 더 많이 소비한다. 그리고

인라인화를 제한하고 간접 함수 호출을 산출하는 구현 세부사항 때문에, std::function 객체를 통해서

클로저를 호출하는 것은 거의 항상 auto로 선언된 객체를 통해 호출하는 것보다 느리다.

 

3) 형식 단축(type shortcut)이라고 부르는 것과 관련된 문제를 피할 수 있다.

std::vector<int> v;

unsigned sz = v.size();

v.size()의 공식적인 반환 형식은 std::vector<int>::size_type인데 64bit Windows에서는 unsigned 32비트인

반면에 std::vector<int>::size_type 64비트이다.

 

 

4) 형식 불일치의 또 다른 예

std::unordered_map<std::string, int> m;

...

for (const std::pair<std::string, int>&p : m)

{
... // p
로 뭔가를 수행

}

합당해 보이는 코드지만 문제점이 있다. std::unordered_map의 키 부분은 const. 따라서 해시테이블에 담긴 std::pair형식은 std::pair<std::string, int>가 아니라 std::pair<const std::string, int>이다.

따라서 컴파일러는 루프의 각 반복에서 p를 묶고자 하는 형식의 임시 객체를 생성하고, m의 각 객체를 복사하고, 참조 p를 그 임시 객체에 묶음으로써 그러한 변환을 실제로 수행한다. 그 임시 객체는 루프의 반복의 끝에 파괴된다.

 

초기화 표현식의 형식이 변하면 자동으로 형식이 변한다 - 이는 리팩토링의 수월해짐을 의미한다.

https://vambii.tistory.com/10

https://channel9.msdn.com/Events/TechDays/TDK2015/T4-5

 

모던 C++ = C++11/14

 

고치고 싶다하지만

-이미 고치기엔 길어져버린 코드

-어디서부터 손을 써야 할지 모름

-코드는 점점 산으로

 

 

 

어디에 기름칠을 해볼까?

-전처리기

-리소스 관리

-함수

-타입, 반복문

 

*전처리기

#if, #ifdef, #ifndef, #elif, #else

많이 쓸수록 복잡, 이해불가, 유지보수 힘듬

 

*케이스 바이 케이스

-타입에 따른 조건부 컴파일은 함수 템플릿을 통해 개선한다.

-하지만 #ifdef를 사용해야 되는 경우도 있다.

           -32비트 vs 64비트 코드

           -DEBUG 모드 vs Non-DEBUG 모드

           -컴파일러, 플랫폼, 언어에 따라 다른 코드

-반드시 사용해야 된다면, 코드를 단순화하는 것이 좋다.

           -중첩 #ifdef를 피하고, 함수의 일부를 조건부 컴파일에 넣지 않도록 한다.

 

 

 

 

 

 

 

 

*매크로

-변수 대신 사용하는 매크로 : #define RED 1

-함수 대신 사용하는 매크로 : #define SQUARE(x) ((x) * (x))

-수많은 문제를 일으키는 장본인

-컴파일러가 타입에 대한 정보를 갖기 전에 계산됨

-필요 이상으로 많이 사용

 

Enum color_type

{

           red, orange, yellow, green, blue, purple, hot_pink

};

*열거체의 문제점

-묵시적인 int 변환

-열거체의 타입을 명시하지 못함

-이상한 범위 적용

-> 열거체 클래스(enum class)의 등장!

Enum class color_type

{

           red, orange, yellow, green, blue, purple, hot_pink

};

Void g(color_type color);

Void f()

{

           Int x = color_type::hot_pink; // cannot convert from ‘color_type’ to ‘int’

           Int x = static_cast<int>(color_type::hot_pink);

}

*열거체 클래스를 사용하자

*묵시적인 int 변환

-> 명시적인 int 변환

*열거체의 타입을 명시하지 못함

-> 타입 명시 가능

*이상한 범위 적용

-> 범위 지정 연산자를 통해 구분

 

*함수 대신 사용하는 매크로

#define make_char_lowercase(c) \

           ((c) = (((c) >= ‘A’) && ((c) <= ‘Z’)) ? ((c) – ‘A’ + ‘a’) : (c))

void make_string_lowercase(char* s)

{

           while (make_char_lowercase(*s++))

                     ;

}

 

#define make_char_lowercase(c) \

           ((c) = (((c) >= ‘A’) && ((c) <= ‘Z’)) ? ((c) – ‘A’ + ‘a’) : (c))

void make_string_lowercase(char* s)

{

           While (((*s++) = (((*s++) >= ‘A’) && ((*s++) <= ‘Z’))

                                ? ((*s++) – ‘A’ + ‘a’) : (*s++)))

                     ;

}

 

#define make_char_lowercase(c) \

           ((c) = (((c) >= ‘A’) && ((c) <= ‘Z’)) ? ((c) – ‘A’ + ‘a’) : (c))

Inline char make_char_lowercase(char& c)

{

           If (c > ‘A’ && c < ‘Z’)

           {

                     c = c – ‘A’ + ‘a’;

           }

           return c;

}

 

*열거체, 함수를 사용하자

-변수 대신 사용하는 매크로에는 열거체를 사용하자

           -열거체에서 발생할 수 있는 문제는 enum class로 해결할 수 있다.

           -열거체 대신 ‘static const’ 변수를 사용하는 방법도 있다.

-함수 대신 사용하는 매크로에는 함수를 사용하자.

           -읽기 쉽고, 유지보수하기 쉽고, 디버깅하기 쉽다.

           -성능에 따른 오버헤드도 없다.

 

*인라인 함수 (inline function)

-함수 내부의 코드를 재사용할 수 있다.

-인스턴트 코드보다 함수에서 코드를 변경하거나 업데이트하기가 더 쉽다.

-함수 이름을 통해 코드가 무엇을 의미하는지 이해하기 더 쉽다.

-함수는 함수 호출 인수가 함수 매개 변수와 일치하는지 확인하기 위해 타입 검사를 한다.(매크로는 안한다.)

-함수는 프로그램을 디버그 하기 쉽게 만든다.

함수는 함수가 호출될 때마다 발생하는 일정량의 성능 오버헤드가 있다는 단점이 있는데 이는 cpu가 다른 레지스터와 함께 실행 중인 현재 명령어의 주소를 저장해야 하므로(나중에 반환할 위치를 알 수 있도록) 모든 함수 매개 변수를 생성해야 한다. 할당된 값을 사용하면 프로그램이 새 위치로 분기된다. 내부에서 작성된(인스턴트 코드)가 훨씬 더 빠르다.

 

크거나 복잡한 태스크를 수행하는 함수의 경우 함수 호출의 오버헤드는 함수가 실행되는 데 걸리는 시간과 비교할 때 중요하지 않다. 그러나 일반적으로 사용하는 작은 함수의 경우, 함수 호출에 필요한 시간이 실제로 함수 코드를 실행하는데 필요한 시간보다 훨씬 많은 경우가 있다. 이로 인해 상당한 성능 저하가 발생할 수 있다.

 

C++은 인라인 함수(inline function)라는 내부에서 작성된 코드의 속도와 함수의 장점을 결합하는 방법을 제공한다. Inline 키워드는 컴파일러에서 함수를 인라인 함수로 처리하도록 요청한다. 컴파일러가 코드를 컴파일하면 모든 인라인 함수가 in-place 확장된다. , 함수 호출이 함수 자체의 내용 복사본으로 대체되어 함수 오버헤드가 제거된다! 단점은 인라인 함수가 모든 함수 호출에 대해 적절한 위치에서 확장되므로 인라인 함수가 길거나 인라인 함수를 여러 번 호출하는 경우 컴파일된 코드를 약간 더 크게 만들 수 있다는 것이다.

 

https://channel9.msdn.com/Events/TechDays/TDK2015/T4-5 21:24

 

C++11(VS2015)부터 throw()deprecated 되고, noexcept 키워드가 추가되었다.

noexcept 키워드는 연산자(operator)의 형태로, 그리고 한정자(specifier)의 형태로 제공된다.

Noexcept() 한정자는 모든 면에서 throw()보다 강력해졌고,

특히 C++11들의 Standard library들을 사용함에 있어, noexcept 한정자는 성능상의 추가 이득을 제공하기도 한다.

 

C++11 이후 std::vector뿐 아니라 STL 컨테이너들은 move semantics가 모두 적용되어 있다.

원소에 대한 이동 처리를 할 때 해당 원소가 movenoexcept를 지원하지 않으면,

Move semantics가 아닌 copy semanticselement를 처리한다.

, 이동 처리에 대한 strong exception guarantee가 되어 있지 않으면 move semantics의 장점을 포기하는 것이다.

 

새롭게 변한 C++언어는 함다식, 알밸류(RVALUE) 레퍼런스, 비동기 프로그래밍 등을 지원한다.

Comments