본문 바로가기

Prog. Langs & Tools/C++

[C++] Ch13. C++ 최신 문법 - Part1

이번 포스팅에서는 STL 알고리즘에 대한 간단할 설명 및 C++ 최신 문법에서 추가된 키워드, 자료형, STL 등에 대해서 알아보고자 한다.

 

STL 알고리즘

STL 알고리즘이란 요소 범위에서 쓸 수 있는 함수들을 의미한다. 따라서 배열 또는 몇몇 STL 컨테이너에서 쓸 수가 있다. 이 때 반복자를 통해 컨테이너에 접근한다. 컨테이너의 크기를 변경하지 않기 때문에 추가 메모리 할당도 없다.

유형별로 다음과 같이 호출하는 include 문이 다르다.

  • #include
    • 변경 불가 순차(sequence) 연산 : find(), for_each()
    • 변경 가능 순차 연산 : copy(), swap()
    • 정렬 관련 연산 : sort(), merge()
  • #include
    • 범용 수치 연산 : accumulate()

예를 들어 vector를 다른 vector로 복사하는 기능을 구현할 때 copy()라는 메서드를 사용하면 다음과 같이 한 줄로 가능하다.

int main()
{
    std::vector<int> scores;
    // ...
    std::vector<int> copiedScores;
    copiedScores.resize(scores.size());

    std::copy(scores.begin(), scores.end(), copiedScores.begin());

    // ...
}  

copy()의 구현은 다음과 같이 이루어진다.

template<class _InIt, class _OutIt> _OutIt copy(_InIt _First, _InIt _Last, _OutIt _Dest)
{
  for (; _First != _Last; ++_Dest, (void)++_First)
  {
    *_Dest = *_First
  }
  
  return (_Dest);
}

STL 알고리즘은 너무 많아서 하나하나 다 설명할 수는 없고, C++ 공식 문서에서 필요할 때 찾아서 써보자.

 

새로운 키워드 (C++11~)

auto 키워드

  • 자료형을 추론
  • JavaScript 등의 언어에 있는 동적인 형(type)과는 다름
  • 실제 자료형은 컴파일 하는 동안 결정됨
  • 따라서, 반드시 auto 변수를 초기화 해야 함
auto x; // 에러
auto x = "Chris"; // 올바름

auto를 사용하여 포인터와 참조를 받을 수 있다. 포인터를 받을 때는 auto 또는 auto*, 참조를 받을 때는 auto&를 사용한다.

// Main.cpp

Cat* myCat = new Cat("Coco", 2);
auto myCatPtr = myCat; // myCatPtr와 myCat은 동일한 포인터이다.

auto를 사용해서 포인터와 값을 다 받을 수 있지만, 가독성을 고려해서 포인터형을 받을 때는 auto*를 사용한다.

참조를 할 때는 똑같이 auto를 사용하면 컴파일러가 구분할 방법이 없으므로 auto&를 사용한다.

// Main.cpp

Cat myCat("Coco", 2);
Cat& myCatRef = myCat;
auto& anotherMyCatRef = myCatRef; // &myCat, &myCatRef, &anotherMyCatRef는 모두 같은 참조값을 가짐

또한 const를 쓰지 않아도 const 참조를 auto로 받을 수 있지만 가독성을 좋게 하기 위해 가능하면 const auto&로 const 참조를 받는다.

auto는 반복자를 쓸 때 쓰면 특히 유용하다. 복잡한 형을 간단하게 표현할 수 있고 그렇게 바꿔 주어도 문제가 되지 않는다.

auto의 베스트 프랙티스는 정리하면 다음과 같다. 가독성 중심으로 적어 보았다.

  1. auto보다 실제 자료형 사용을 권장한다.
  2. 예외가 있다면 템플릿 매개변수와 반복자에는 auto를 사용한다.
  3. auto보다 auto*를 사용한다.
  4. auto&보다 const auto&를 사용한다.

 

static_assert 키워드

assert는 코드의 실행을 멈추는 코드로서, 디버깅을 할 때 사용한다. 코드 실행이 해당 함수 안에서 멈춘다. 오직 디버그 빌드에서만 작동한다. 릴리즈 빌드에는 사라져서 최종 제품에는 영향을 미치지 않는다.

// Class.cpp
#include <cassert>
const Student* Class::GetStudentInfo(const char* name)
{
    assert(name != NULL); // 조건이 false면 실행을 멈춤
    // 이하 코드 생략..
}

static_assert는 컴파일 중에 어서션을 평가한다. 컴파일러가 assert 조건이 참인지 아닌지 알 수 있다. 만약 실패하면 컴파일러는 컴파일 에러를 뱉는다. c++11에서 아주 유용한 기능 중 하나이다.

static_assert의 베스트 프랙티스는 다음과 같다

  • 최대한 assert보다 static_assert를 사용한다.
    • 모든 곳에 static_assert를 쓰자. 그리고 나서 남은 모든 곳에 assert를 쓰자.
  • assert를 사용한다면 프로그램을 실행 시켜야 하며, 실행 도중(디버그 빌드)에서만 어서션을 포착한다.
  • static_assert를 사용한다면 컴파일 중에 모든 문제를 즉시 알아챈다.

 

default/delete 키워드

default 키워드를 사용하면, 컴파일러가 특정한 생성자, 연산자 및 소멸자를 만들어 낼 수 있다. 그래서 비어 있는 생성자나 소멸자를 구체화 할 필요가 없다. 또한 기본 생성자, 연산자 및 소멸자를 더 분명하게 표시할 수 있다.

// Dog.h

class Dog
{
public:
    Dog() = default;
    Dog(std::string name);
private:
    std::string mName;
};

// Dog.cpp
#include "Dog.h"

// 컴파일러가 Dog()를 대신 만들어 냄

Dog::Dog(std::string name)
{
    // 이하 코드 생략
}

컴파일러가 자동으로 생성자를 만들어 주길 원치 않는다면 delete 키워드를 사용한다. private 접근 제어자로 빈 생성자를 만드는 트릭은 권장하지 않는다. 이렇게 할 경우 올바른 에러 메시지가 나온다.

// Dog.h
class Dog
{
public:
    Dog() = default;
    Dog(const Dog& other) = delete;
    // ...
};

// Main.cpp
#include "Dog.h"
int main()
{
    Dog myDog;
    Dog copiedMyDog(myDog); // error
    // ...
}

컴파일러가 코드를 생성하는 암시적인 방식에 더 이상 기댈 필요가 없이 default/delete 키워드를 통해 명확하게 제시하자.

 

final/override 키워드

상속을 막기 위해 사용하는 키워드이다. 아무나 상속을 하게 되면 메모리 관리가 제대로 되지 않고 소멸자를 가상으로 만들어야 하는 문제가 있다. final 키워드를 통해 클래스의 상속을 막을 수가 있다.

final 키워드는 클래스나 가상 함수를 파생 클래스에서 오버라이딩(마지막 수정) 못 하도록 하려면 final 키워드를 사용한다. 컴파일 도중에 확인하며 당연히 가상 함수가 아니면 쓸 수 없다.

// Animal.h
class Animal final
{
public:
    virtual void SetWeight(int weight);
    // ...
}

// Dog.h
#include "Animal.h"
class Dog: public Animal
{
public:
    virtual void SetWeight(int weight);
    // ...
};

잘못된 가상 함수 오버라이딩을 막으려면 override 키워드를 사용한다. 당연히 가상 함수가 아니면 쓸 수 없으며, 이 역시 컴파일 도중에 검사한다.

// Animal.h
class Animal
{
public:
    virtual void SetWeight(float weight);
    void PrintAll();
    // ...
};

// Dog.h
#include "Animal.h"

class Dog :public Animal
{
public:
    // OK
    virtual void SetWeight(float weight) override;
    
    // 컴파일 에러
    virtual void SetWeight(int weight) override;
    
    // 컴파일 에러: 가상 함수가 아님
    void PrintAll() override;
    
    // ...
};

 

참고자료

  1.  <C++ 언매니지드 프로그래밍> 포큐 아카데미