Modern C++

Dohun Lee·2025년 8월 18일

C/C++

목록 보기
34/34

Modern C++은 일반적으로 C++11 이후 버전들의 문법들을 의미한다.

auto

auto 타입을 사용하면 컴파일 단계에서 컴파일러가 자체적으로 대입된 값의 타입을 바탕으로 타입을 추론해서 적절한 타입으로 교체해준다.

template 그리고 auto 같이 타입을 컴파일 단계에서 자동으로 추론 해주는 것을 type deduction 이라고 한다. 하지만, auto에는 복잡한 추론 규칙도 존재하고, 모든 타입을 암시적으로 추론해주지는 않는다.

auto 사용 시 주의점

기본적으로 auto는 대입 된 값의 타입의 const와 &(참조)를 무시한다. 만약, auto 타입의 변수에 const 혹은 &타입의 변수를 대입하면, const, &를 빼고 타입을 추론한다.

즉, 참조 타입의 값을 auto에 대입한다면, 참조 타입으로 변환 되는것이 아니라, 일반적인 변수 타입으로 변환되어서 값이 참조 값이 되는것이 아니라, 대입된 값을 복사 하게 된다.

그래서 const, & 타입의 변수를 대입 할 때는 const auto 혹은 auto& 처럼 명시적으로 const, &타입을 추론하도록 해 줘야 한다. 또한 auto를 남발하게 되면, 코드 가독성이 매우 떨어지기 때문에 적절한 곳에 적절하게 사용 해야 한다.

nullptr

전통적으로는 NULL, 즉 0을 빈 포인터에 대입하였다. 하지만 포인터형 변수에 정수형 값인 0을 대입하는 것이 문제가 될 수 있는데, 이를 해결하기 위해서 등장한 것이 바로 nullptr이다.

nullptr은 단순한 정수 값이 아닌 하나의 객체이다. 아직 할당되지 않은 포인터에 nullptr을 대입해서 해당 값이 확실하게 빈 값이라는 것을 표시 할 수 있게 되었다.

using

Modern C++에서 using 키워드는 typedef의 문제점을 해결하기 위한 문법으로 등장 하였다. typedef는 를 사용하여 타입을 재정의 하게되면, 비교적 해석하기 힘들고 직관적이지 않다는 단점이 있다. 하지만 using을 사용하게 되면, using ID = int 와 같이 직관적으로 이해하기 편한 코드를 작성 할 수 있다.

또한 typedef과 템플릿을 함께 사용하게 된다면, typedef로는 불가능한 템플릿 별칭을 선언 할 수 있다. 템플릿 별칭을 사용하여, using SmartPointer = std::unique_ptr<T, MyDeleter> 와 같이 선언 한 후, 사용 할 수 있다.

enum class

enum class는 기존 enum의 단점을 극복하고, 변수 이름의 범위 관리를 용이하게 하기 위해서 만들어 졌다.

기존 enum은, unscoped enum 즉, 내부에서 사용한 이름을 enum 외부의 스코프에서도 사용하지 못하는 단점이 있었는데, enum class는 이러한 단점을 극복하고, enum class 내부에서 사용한 이름도 외부에서 새로 사용할 수 있게 되었다.

enum PlayerType{
	Player,
	...
}
위와 같이 Player라는 이름을 enum 내부에서 사용하였다면, 
외부에서 같은 이름을 사용하지 못하는 문제가 발생했었다.

또한, 기존 enum은 암묵적으로 정수형으로 변환되었지만, enum class는 캐스팅을 통해 명시적으로 정수형으로 변환 해 주어야 한다. 또한 enum class는 EnumClass::Enum와 같이 해당 enum에 접근 해야 한다.

삭제된 함수 - delete

delete 키워드는 객체 내부에서 특정 함수, 연산자를 허용하지 않도록 하는 키워드이다.

예를 들면, 클래스 객체 사이의 복사 대입을 막고싶다면, delete 키워드를 사용해서 해당 생성자에 접근을 막을 수 있다.

기본적으로는 private 영역에 해당 함수, 연산자에 대한 접근을 막을 수 있지만, friend 키워드와 같은 접근 할 수 있는 방법들이 존재 하기 때문에, 완전히 해당 함수와 연산자에 대한 사용 자체를 막고싶다면 delete키워드를 사용 해야 한다.

delete 키워드는 사용을 막고 싶은 함수 혹은 연산자를 대상으로, void operator=() = delete; 와 같이 사용하면 된다.

override, final

override, final 키워드는 가상 함수와 연관된 문법이다. 이 키워드를 설명하기 위해선 먼저 override가 무엇인지를 알아야 한다.

  • overriding: 상속 받은 클래스의 virtual 함수 재정의
  • overloading: 함수 이름의 재사용 (함수의 이름만 재사용 하고 인자 구성을 다르게)
    기존의 virtual 함수를 정의 시에 상속 받은 함수가 원본인지, 그 위의 부모가 원본 인지를 판단하기 힘들다.

예를 들어 부모 클래스의 함수에 virtual 키워드를 붙이면, 해당 클래스를 상속 받은 자식 클래스들의 해당 함수는 모두 자동으로 virtual 키워드가 붙게 된다. 하지만, 이 상황에서 원본 함수와 상속 받은 함수가 모두 virtual 키워드만 붙은 가상 함수이기 때문에 무슨 함수가 원본 가상 함수 인지 구별하기가 쉽지 않다.

이러한 상황에서 원본 가상 함수인지, 아니면 부모 클래스의 가상 함수를 overriding한 가상 함수 인지를 판단하기 위해서 override 키워드를 사용한다. 간단하게, 상속받은 가상 함수의 뒤에 void func() override 와 같은 방법으로 키워드를 붙이면 된다.

final 키워드는 가상 함수를 overriding 하고 이 이후의 다음 자손들의 해당 함수 overriding를 막는 키워드이다.

전달 참조 (forwarding reference)

함수 인자로 &&타입이 있다면, 이 타입의 의미는 오른값 참조만을 의미 하는 것이 아니라, 왼값 참조로도 해석될 여지가 있다.

템플릿이 포함된 함수에선 인자를 &&타입으로 지정 할 경우, move 없이 객체만 전달 할 경우 왼값 참조로 넘어간다.

template <typename T>
void ForwardRef(T&& parm){
	...
}

Object obj;
ForwardRef(std::move(obj)); -> 오른값 참조로 넘어간다.
ForwardRef(obj); -> 일반적인 참조 타입으로 넘어간다.

위와 같은 상황에서 ForwardRef 함수 내부에서 parm 값의 타입이 오른값 참조인지 왼값 참조 인지를 구분 해야하는데, 이때 사용하는 함수가 std::forward 함수이다.

template <typename T>
void ForwardRef(T&& parm){
	...std::forward(parm);
}

함수 내부에서 위와 같이 std::forward를 사용해서 parm을 처리하게 되면, 만약 parm이 오른값 참조 형태라면, std::move를 통해 처리해주고, 왼값 참조라면 일반적인 대입 연산으로 처리 해 준다.

위와 같이 처리해 주어야 하는 이유는, 함수의 인자로 오른값 참조를 받아도, 함수 내부에서 그 인자의 값은 오른값 참조, 즉 진정한 의미의 오른값이 아니다. 그렇기 때문에, 인자로 받은 값을 일반적인 대입 연산으로 처리하게 되면, 값이 이동하는게 아니라 복사 되게 된다. 그래서 인자로 받은 오른값 참조를 대입 할 때, std::move를 통해 대입 해 주여야 한다.

{} 중괄호 초기화

중괄호 초기화란, ()가 아닌 {}를 통해 객체를 초기화 하는 것을 말한다.

Myclass c1;
MyClass c2 { c1 }; ->이렇게 초기화 가능
vector v{1, 2, 3} -> 이렇게 이용 가능

중괄호 초기화의 이점은 축소 변환 방지 {}를 통해 변수를 초기화 하면, 컴파일러가 축소 변환을 더 잘 감지한다는 것이다.

중괄호 초기화의 또 다른 특징은 MyClass c()를 통해 초기화 하는 것은 기본 생성자를 호출하는 것이 아니지만, MyClass c{} 이렇게 중괄호로 초기화를 하면 기본 생성자를 호출 한다는 점이다.

initailizer_list

initializer_list란, 함수의 인자로 여러개의 값을 한번에 받을 수 있도록 하는 컨테이너 이다. 이 컨테이너를 이용하면, Class c = {1, 2, 3} 과 같이 중괄호 초기화로 여러 값을 한번에 넣는 초기화가 가능해진다.

하지만, 중괄호 초기화를 이용해 클래스를 초기화 한다면, 만약 (int, int) 와 같은 인자를 받는 생성자가 있을때, initializer_list를 인자로 가지는 생성자가 존재하면, initializer_list인자의 생성자가 호출 우선권을 가지게 된다.

다시말해, Class c = {1, 2} 를 호출 하게 되면, (int, int)형의 인자를 받는 생성자가 호출 되는 것이 아니라, initializer_list를 인자로 받는 생성자가 호출 된다.

람다 (lamda)

lamda 문법이란, 함수 객체를 빠르게 만드는 문법이다.

람다 함수의 기본적인 형태는 [] (파라미터) { 내부 로직 } 과 같다.
예를 들어, 인자로 int 참조를 가지고, 해당 int5를 더하는 람다 함수는 다음과 같다.
[] (int& num) { return num + 5; }

람다 함수는 함수 포인터, 객체와 같이 변수에 대입 할 수도 있다. 
이때는 보통 복잡한 타입 선언 대신 auto 타입을 사용한다.
auto lam = [] (파라미터) { 내부 로직 };
위와 같은 객체를 람다 객체 (closure),, 람다의 실행 시점 객체라고 한다.

또한 람다 함수는 외부의 변수를 람다 함수 내부에서 사용 할 수 있는데, 
이러한 방식을 capture 라고 한다.

[내부 저장 멤버 변수] (파라미터) { 내부 로직 }; -> [] 
= capture 함수 객체 내부에 변수를 저장하는 개념과 유사
캡처 모드는 어떤 방식으로 인자를 가져올 것인가를 결정하는데, 두가지 방법이 있다.

[=]: 값 복사, [&]: 값 참조 
-> 이러한 방식은 가져오는 모든 인자를 해당 방법으로 사용한다는 의미인데, 
이러한 방법을 지양하고 아래와 같이 사용하는걸 추천한다. 
(명시적으로 어떤 변수를 사용하고 어떤 캡처모드로 사용할지를 표시)

변수마다 캡처모드를 지정해서 사용
값 방식 [name], 참조 방식 [&name]
[num1, num2&, item&] -> num1은 값 복사, num2, item은 값 참조 방식으로 사용

스마트 포인터 (smart pointer)

스마트 포인터란, 포인터를 정책에 따라 관리하는 객체, 즉, 포인터 wrapper이다. 스마트 포인터는 객체가 스코프를 벗어날때 자동으로 할당 해제된다.

shared_ptr

shared_ptr엔 내부에 참조 카운트가 존재한다. 이 참조 카운트는 해당 포인터가 참조 될 때 마다 1 씩 증가하게 되는데, 해당 카운트가 0이 되어야 해당 객체가 소유한 포인터가 할당 해제 되도록 구현되어 있다.

shared_ptr의 소멸 시점에 참조 카운트가 1씩 감소하고, 만약 소멸 되는 shared_ptr의 참조 카운트가 0이라면, 이때 해당 shared_ptr에 있는 포인터의 할당 해제가 이루어 진다.

shared_ptr은 make_shared를 통해서 할당해주는 것이 좋은 성능을 낼 수 있다.

shared_ptr에도 문제점이 있는데, 이는 사이클 문제이다. 만약 두 객체가 내부에서 서로를 가리키고 있는 포인터를 지닌다면, 이 두 객체 내부의 shared_ptr의 참조 카운트가 절대 1 아래로 감소하지 않아서 해당 포인터가 소멸되지 않는 문제가 발생 할 수도 있다.

이때는 해당 shared_ptr을 nullptr로 대입하여 순환 구조를 강제로 끊어주거나, weak_ptr을 사용해주면 된다.

weak_ptr

shared_ptr의 순환 구조 문제 해결을 위해서 사용 할 수 있는 스마트 포인터의 한 종류이다. shared_ptr이 관리하는 포인터를 참조 하지만, 소유권을 가지지 않는다. 즉, shared_ptr을 weak_ptr에 대입하여도 참조 카운트가 증가하지 않는다. 이는 객체의 수명에 영향을 주지 않는다는 특징이 있다.

weak 포인터를 통해 작업을 하려면 expired() 포인터의 유효성을 확인 후 lock()을 통해 sahred_ptr로 변환 해서 사용 해야 한다.

unique_ptr

unique_ptr는 간단하게 말하면, 무조건 하나만 존재할 수 있는 포인터이다. 즉, 다른 포인터로의 대입이 불가능하다. 즉, 하나의 객체만 소유권을 가질 수 있다는 것이다. 그렇기 때문에 소유권을 옮길땐 std::move를 통해 이동 연산을 해야 한다.

profile
미국 공대생

0개의 댓글