이제 C++20부터는 vector, string과 같은 표준 컨테이너들에도 constexpr을 적용할 수 있게 되었다.
이뿐만 아니라, sort, find 등 대부분의 표준 알고리즘 함수들도 constexpr로 지원된다.
이 변화가 갖는 의미는 단순히 "좀 더 많은 곳에서 constexpr을 쓸 수 있게 되었다"가 아니다.
이제는 복잡한 자료구조 조작, 심지어 컨테이너와 알고리즘을 사용한 계산까지 컴파일 타임에 처리할 수 있다는 뜻이다.
constexpr int Test()
{
vector<int> v{1, 2, 9, 6, 4};
sort(v.begin(), v.end());
return v.back();
}
int main()
{
// 가능
constexpr int i = Test();
}
이 모든 과정이 런타임이 아니라 컴파일 타임에 실행된다.
즉, i의 값은 컴파일 시점에 이미 결정되어, 프로그램이 실행될 때는 단순히 상수처럼 사용된다.
C++20에 와서는 사실상 대부분의 STL 컨테이너와 알고리즘까지 지원하게 되었다.
C++20부터는 배열을 선언할 때 std::to_array를 사용할 수 있게 되었다.
이전에는 std::array<int, 3>처럼 타입과 크기를 명시해야 했지만, 이제는 auto와 std::to_array를 조합해서 더 간결하게 선언할 수 있다.
// C++20 이전 버전
array<int, 3> arr2{ 1, 2, 3 };
// C++20 버전
auto arr1 = std::to_array({1, 2, 3});
이 방식은 타입과 배열 크기를 일일이 적지 않아도 되고, 형식 연역(auto) 덕분에 코드가 더 깔끔해진다.
즉, 더 유연하고 편리하게 std::array를 선언할 수 있는 새로운 방법이다.
배열을 동적으로 할당할 때는 항상 new와 delete[ ]를 짝맞춰 사용해야 한다.
Widget* wd = new Widget;
...
delete wd;
string* str = new string[100];
...
delete[] str // 객체의 배열을 삭제
이렇게 사용자는 직접 할당과 해제 코드를 관리해야 한다.
하지만 실수로 delete[ ]를 빠뜨리거나, 잘못된 방식으로 해제하면 메모리 누수가 발생한다.
shared_ptr을 사용하면 소멸 시 자동으로 자원을 해제해주기 때문에, 개발자가 직접 delete나 delete[ ]를 신경 쓸 필요가 없다.
C++20 이전까지는 shared_ptr로 단일 객체만 안전하게 관리할 수 있었지만, 이제 C++20부터는 배열도 손쉽게 관리할 수 있다.
std::shared_ptr<double[]> shared_arr = std::make_shared<double[]>(1024);
shared_arr[1] = 1.0;
배열도 shared_ptr로 관리할 수 있게 되면서, 배열 메모리 관리의 실수 위험을 크게 줄이고, 코드도 더 깔끔하게 작성할 수 있게 되었다.
앞으로는 동적 배열도 shared_ptr와 함께 쓰는 것을 적극 활용해볼 수 있다.
vector에서 음수와 같은 특정 조건의 요소만 골라서 삭제하려면,
과거에는 반복자를 직접 돌리며 수동으로 erase를 호출하는 코드가 일반적이었다.
vector<int> vec{ -1, 2, -3, 4, -5 };
for (auto it = vec.begin(); it != vec.end();)
{
int value = *it;
if (value < 0)
{
it = vec.erase(it);
}
else
{
++it;
}
}
이 코드는 문제 없이 동작하긴 하지만, 삭제와 반복자의 관리, 실수할 여지 등이 있어서
조금 더 세련된 방법이 필요했다.
그래서 이후에는 다음처럼 remove_if와 erase를 조합해서 자주 사용했다.
auto newEnd = std::remove_if(vec.begin(), vec.end(), [](int num) { return num < 0; });
vec.erase(newEnd, vec.end());
이 방식은 STL이 추천하는 패턴이었고, 많은 개발자가 익숙하게 사용해왔다.
이제 C++20에서는 이 과정을 더 간단하게 만들 수 있다.
바로 std::erase_if를 사용하는 것이다.
std::erase_if(vec, [](int num) { return num < 0; });
erase_if는 조건에 맞는 요소를 찾아서 삭제하고 나머지 요소를 자동으로 벡터 앞쪽으로 이동시킨다.
C++20의 erase_if는 더 읽기 쉽고, 안전하고, 생산성을 높여주는 기능이다.
앞으로 조건부 삭제가 필요한 경우라면 적극적으로 사용해보면 좋다.
과거에는 set이나 map에서 특정 요소가 있는지 확인하려면
반드시 find()를 사용하고, 그 반환값을 end와 비교하는 패턴을 사용해야 했다.
std::set s{ 1, 2, 3, 4, 5 };
std::map<int, int> m{ {1, 1000}, {2, 2000} };
auto findIt = s.find(2);
if (findIt != s.end())
{
cout << "찾음" << "\n";
}
이 방식은 익숙하긴 하지만, 항상 반복적으로 find()와 end()를 비교해야 하는 번거로움이 있었다.
C++20부터는 contains()라는 메서드가 새로 추가되어 더 직관적이고 간단하게 요소 존재 여부를 확인할 수 있다.
if (s.contains(2))
{
cout << "찾음" << "\n";
}
if (m.contains(2))
{
cout << "찾음" << "\n";
}
이제 contains에 원하는 값을 넘겨주기만 하면 그 값이 set 또는 map에 들어있는지 바로 알 수 있다.
코드가 훨씬 간결해지고 의도가 명확해졌다는 점에서 contains는 C++20에서 반가운 변화 중 하나다.
이번에는 다양한 컨테이너에서 C++20을 통해 추가된 새로운 기능들을 살펴보았다.
사실 모든 기능이 항상 필요한 것은 아닐 수도 있다.
직접 사용해보면 “굳이 이렇게까지 해야 하나?” 싶은 부분도 있을지 모른다.
그렇지만 erase_if나 contains 같은 기능들은 실제로 코드를 작성하다 보면 종종 필요함을 느끼게 되는 유용한 도구들이다.
이번 정리는 짧지만, 앞으로 코딩을 하면서 이런 새로운 기능들이 필요할 때
바로 꺼내 쓸 수 있도록 한 번쯤은 정리해두고 싶었다.