C++

 

C++

C언어와 호환성이 있으며 객체지향과 일반화 프로그래밍과 같은 멀티 패러다임을 지원하는 시스템 프로그래밍 언어


1 특징[ | ]

  • C언어와 호환성이 있다.
  • 객체지향과 일반화 프로그래밍을 모두 아우르는 멀티 패러다임을 지원한다.
  • zero-cost abstraction[1]를 설계 이념으로 하여 고성능을 추구하므로, 자바C#과 같은 언어보다 빠르다.
  • 다른 언어보다 어렵고 복잡하며 비교적 생산성이 낮다고 평가받는다.[2]

1.1 C언어와의 관계[ | ]

  • C언어와 호환성이 있지만 설계 지향점이 다르기 때문에, 전혀 다른 언어로 보는 것이 안전하다.
  • C언어를 부분집합으로 갖지만, 일부 과거 버전은 그렇지 않았던 경우도 있었다.

2 일반화 프로그래밍[ | ]

C++에서는 템플릿을 사용한 일반화 프로그래밍이 지원되며, 범용 라이브러리 등 매우 많은 부분에서 사용되고 있다.

템플릿은 제네릭보다 크게 확장된 개념으로, 자바나 C#의 제네릭보다 많은 역할을 수행한다. 컴파일 타임에 값을 미리 계산하고, 인자의 타입에 따라 다른 코드나 반환값을 선택하는 등을 할 수 있다.

사실상 성능을 위해 컴파일 시간에 전부 처리되는, 하나의 작은 프로그래밍 언어라고 봐도 무방하다. 이와 같이 컴파일 타임에 수행되는 프로그래밍을 메타 프로그래밍이라고 한다.

C++의 표준 라이브러리를 사용한다면, 무조건 템플릿을 사용하게 된다. 예를 들어, 동적 배열 객체인 'vector<int>'은 int 타입에 특수화하여 int 배열을 생성한다. 템플릿은 컴파일 시간에 모든 과정이 처리되므로, 실행 시간에 int 대신 short 타입에 특수화할 수는 없다.

3 메모리 관리[ | ]

잘 작성된 코드는 문자열이나 배열과 같은 동적 메모리의 관리를 객체의 수명주기에 묶어서 자동적으로 수행하지만[3], 오래된 코드에서는 이를 포인터를 사용해 전부 수동으로 관리했다.

C++는 자바나 C#과 같은 언어와 완전히 다른 방식으로 메모리 관리를 한다는 것을 유의해야 한다. 자바나 C#에서는 다른 객체에서 객체를 생성해서 넘겨주는 방식이 적지 않게 사용되지만, C++에서 이는 메모리 문제를 일으키기 쉽다. 다만, C++11 이후 추가된 std::shared_ptr와 같이 쓰레기 수집을 참조 횟수 계산 방식으로 지원하거나 std::unique_ptr와 같이 소유권을 넘겨줄 수 있는 스마트 포인터를 활용하면, 이러한 구현 자체가 문제되지는 않는다.

4 성능 제약 요소[ | ]

C++는 zero-cost abstraction[4] 언어이지만 이와는 방향성이 다른 몇몇 기능이 있다. 대표적인 예는 RTTI(Run-Time Type Information)와 예외 처리(try-catch)다.

4.1 RTTI[ | ]

RTTI(Run-Time Type Information)는 컴파일 타임이 아니라 런타임에 객체의 타입이 결정되는 C++의 매커니즘이다. 프로그램이 실행될 때, 객체에 대한 정보를 저장하고 확인해야 하기 때문에 오버헤드가 발생한다. 다음의 문법이 해당하는 예이다.

  • dynamic_cast
  • virtual

dynamic_cast의 경우 해당하는 상속 트리를 확인해야 하기 때문에 적지 않은 성능 하락이 발생한다. 하지만 이에 의존하는 언어의 기능이나 라이브러리가 없기 때문에 사용하지 않아도 된다. 컴파일러에서 기능을 제외하는 옵션도 제공한다.

virtual의 경우 50% 정도의 성능 하락이 있을 수 있지만, 현대 컴퓨터에서는 분기 예측을 잘 활용하여 평균 5~10% 정도의 성능 하락을 보여준다. 그리고 이러한 성능 하락은 어디까지나 함수 호출 비용으로만 계산한 것이기 때문에 특수한 상황이 아니라면 고민하지 않아도 된다. 많은 소프트웨어가 이 문법을 자주 사용한다.

4.2 예외 처리[ | ]

예외도 마찬가지로, 설계상 예외 핸들러를 찾기 위해 (RTTI와 동일한 맥락으로) 던져지는 예외의 타입별로 타입 캐스팅을 해야 하기 때문에 런타임 타입 정보가 필요하다.

따라서 예외를 던질 때마다 적지 않은 성능 하락이 발생한다. 현대 컴파일러에서 if문 처리는 분기 예측에 따라 1~20 cpu cycle를 소모하고, 예외처리는 대략 5000 cpu cycle를 소모한다#는 점을 고려하여, 불필요한 예외는 사용하지 말아야 하며 성능이 중요한 부분에서만 예외 처리를 사용하지 않는 것을 추천한다.

RTTI와 달리 C++의 표준 라이브러리를 사용하고 에러 처리를 동반하는 한 예외는 현실적으로 피할 수 없다. C++에서 객체의 생성이나 복사를 실패시킬 수 있는 문법적 기능은 예외가 유일하며 표준 라이브러리는 이를 적극적으로 활용하기 때문이다.

5 모던 C++[ | ]

C++98 이후 별다른 변화없이 사용되었으나, C++11이 등장하면서 급격하게 변화하기 시작했다.

C++ 표준 위원회는 업데이트가 잘 완성된 후 발표하려 했던 접근 방식 때문에 개정판의 발표가 늦어진 것이라고 자평하고, C++11 이후 기차 모델로 3년마다 개정판을 출판하기로 결정했다. 기존의 C++ 호환성을 어느 정도 유지하면서 점차 개선하고 있다.

5.1 C++11[ | ]

C++11 언어에는 다음의 주요 기능이 추가되었다.

  • 우측값 참조(대입 연산자) 지원[5]
  • 열거형 클래스[6]
  • 범위 기반 for문 지원[7]
  • 람다 함수 지원[8]
  • 변수 타입 추론 지원[9]
  • 속성(attribute) 추가[10]


C++11 표준 라이브러리에는 다음의 주요 기능이 추가되었다.

5.2 C++14[ | ]

C++14는 마이너 버전 성격을 지니며 다음의 주요 기능이 추가되었다.

  • 함수 반환형 추론 지원
  • 람다 함수 개선

5.3 C++17[ | ]

C++17 언어에는 다음의 주요 기능이 추가되었다.


C++17 표준 라이브러리에는 다음의 주요 기능이 추가되었다.

5.4 C++20[ | ]

C++20에는 다음의 주요 기능이 추가될 예정이다.

  • 언어 차원의 비동기 프로그래밍 지원[19]
  • 소스 코드 모듈화 지원[20]
  • 템플릿 매개변수에 명시적 조건을 추가할 수 있는 컨셉트 지원[21]

5.5 시범 사양[ | ]

C++ 표준 워원회가 작성한 시범 사양으로는 네트워크 라이브러리, 리플렉션 그리고 2D GUI 라이브러리 등이 있다. 오래전부터 계속 논의중이지만, 성능 하락이 큰 예외를 대체하는 에러 처리에 대한 구문이나 라이브러리도 추후에 추가될 가능성이 있다. 표준안으로 std::expected이 고려되었지만, 표준에 포함되지 못했다.

6 소스코드 예시[ | ]

#include <iostream>

int main() {
    std::cout << "Hello World !" << std::endl;
    return 0;
}

7 같이 보기[ | ]

8 참고[ | ]

  1. 성능 하락 없는 추상화를 의미한다.
  2. 프로그래머의 자유를 우선시하고 멀티 패러다임을 지원하기 위해 문법의 제약이 적다. 다만, 이로 인해 난이도가 C언어보다도 어려워졌다.
  3. std::string이나 std::vector 모두 이것에 대한 예이며, 이는 C++의 특이점인 RAII(Resource Acquisition Is Initialization) 패턴이 적용된 대표적인 예이다. 스마트 포인터인 std::unique_ptr이나 std::shared_ptr도 이에 해당된다.
  4. 추가적인 시스템 자원을 사용하지 않는 추상화
  5. 우측값 참조는 앝은 복사를 수행한다. 복사되는 개체가 더 이상 사용될 필요가 없는 경우, 개체의 전체를 복사하는 대신 값을 단순히 대입함으로서 오버 헤드를 줄일 수 있다. 컴파일러 차원의 자동 최적화는 코드 손상의 여지가 있기 때문에 지원하지 않는다.
  6. "enum class name : type {}"와 같은 형태로 사용하며 객체의 변수 사용과 거의 동일하다.
  7. 표준 라이브러리의 반복자보다 코드가 간결해진다.
  8. 람다 함수는 코드 블럭이나 함수 안에 삽입 될 수 있으며 참조도 가능하다.
  9. auto 키워드를 통해 타입 이름이 긴 변수를 선언하면, 길이를 줄일 수 있다.
  10. 표준화된 문법으로써, 코드의 각 부분에 추가하여 컴파일러가 이를 참고할 수 있다.
  11. std::shared_ptr와 같은 함수를 통해 쓰레기 수집을 참조 횟수 계산 방식으로 지원한다. C++11에서 std::auto_ptr은 문제점이 있어 사용되지 않음으로 처리되고 C++17에서 제거되었다.
  12. 구조체나 배열을 초기화하기 위해 활용되는 구문인 "{...}"를 std::initializer_list<type>로 입력받을 수 있다.
  13. std::thread로 제공된다.
  14. 기본적으로 ECMAScript의 정규 표현식을 사용하며, grep 나 sed에서 사용되는 정규 표현식도 옵션 인자를 통해 사용할 수 있다. Perl이나 PCRE의 정규 표현식은 표준 라이브러리에서 지원하지 않는다. 그리고 컴파일러들의 구현이 좋지 않은지 파이썬보다도 1.5배나 느리다.
  15. 유니코드를 출력하려면 유니코드 로케일을 운영체제에서 지원해야 하며, 경우에 따라 변환해야 할 수 있다.
  16. C 표준 라이브러리의 time.h에 포함된 시간 함수와 달리, 나노초 단위가 명시되어 있어서 윈도우에서도 나노초 측정이 가능하다.
  17. 유니코드 문자열 선언 자체는 앞서 언급한 바와 같이 이미 C++11에서 추가되었다.
  18. 정렬과 같은 표준 라이브러리의 함수에 이러한 기능이 추가되었다.
  19. C++20 이전이라도 불가능하지는 않지만, 비슷한 문법을 구현하기 위해 Boost와 같은 라이브러리 지원이 필요하다.
  20. 각각의 소스코드에 헤더 파일을 일일이 포함하는 대신, 미리 컴파일된 헤더 파일을 사용하는 것과 같다. 이는 컴파일 시간 단축에 효과적이지만, 매크로의 사용에 제약이 있기 때문에, C++로 작성된 소프트웨어의 소스코드의 구조가 급격하게 변화할 수 있다.
  21. 템플릿 변수의 조건을 일부러 틀리게 하는 트릭을 넣을 필요가 적어지기 때문에, 코드가 간결해지고 가독성이 증가한다.
문서 댓글 ({{ doc_comments.length }})
{{ comment.name }} {{ comment.created | snstime }}