c++

12 minute read

c++ 기본 개념 학습을 위한 정리

상속, 가상함수

상속

오버라이딩: 기반 클래스(부모클래스)의 함수와 같은 이름의 함수를 파생 클래스(자식클래스)에서 정의하여 해당 함수가 호출되게 만드는 것

is-a: 모든 상속관계는 is-a 관계라고 볼 수 있다.

has-a: 한 클래스가 다른 클래스를 가지고 있는 관계

업캐스팅: 파생 클래스를 기반 클래스로 캐스팅하는 것

다운 캐스팅: 기반 클래스를 파생클래스로 캐스팅하는 것

가상 함수

virtual 키워드를 붙여 가상 함수를 사용 할 수 있다. ex)virtual void method()

가상 함수는 어떤 함수를 실행 할 지 런타임시에 결정되는 동적바인딩이 수행된다.

정적바인딩은 컴파일시에 실행할 함수가 정해진다.

가상함수를 오버라이드하는 경우 override 키워드를 이용하여 명시적으로 나타낼 수 있음 ex)void method() override

다형성(polymorphism)은 가상함수를 이용하여 하나의 메소드를 호출하여도 여러 객체에 따라 다른 일을 하게 만드는 것을 의미한다.

가상함수가 포함된 클래스들은 각자 vtable을 가지고 있으며 함수 호출 시 vtable에 정의된 바에 따라 기반 클래스 혹은 파생 클래스의 함수를 호출한다.

순수 가상 함수: 반드시 오버라이딩 해야하는 가상 함수 ex) virtual void speak() = 0;

본체가 없기 때문에, 이 함수를 호출하는 것은 불가능하다.

추상 클래스: 순수 가상 함수를 하나 이상 포함하고 있는 클래스, 순수 가상 함수를 구현하지 않으면 인스턴스화가 불가능하다.

템플릿

template 과 같이 사용

컴파일시 컴파일러가 타입에 맞도록 치환하여 코드로 생성한다.

인스턴스화 되지 않은 코드에서는 치환되지 않는다.(생성되지 않기 때문에)

ex) T -> string, Vector

c++11 using을 이용하면 typedef 대신에 사용가능하다.

typedef void (*func)(int, int);
using func = void (*)(int, int);

auto: 오른쪽의 연산자를 통해서 컴파일러가 타입을 추론, 템플릿과 같은 방식으로 타입을 추론한다.

리터럴 연산자

  • L””: wchar_t[]으로 정의
  • u8””: UTF-8 문자열
  • u””: UTF-16 문자열
  • R”()”: Raw string literal, 모든 문자가 그대로 저장(개행문자, 주석 표시 표함), c++11부터 사용 가능 R"(db-\d*-log\.txt)"
  • "”s: string으로 추론하여 정의, c++14부터 사용 가능 auto str = "hello"s;

정규 표현식 라이브러리(regex)

regex 라이브러리를 include해서 사용 가능

regex_match

생성한 regex 객체를 이용하여 주어진 문자열이 주어진 패턴과 일치하는지 여부를 검사

std::regex re("db-\\d*-log\\.txt");
//std::regex re(R"(db-\d*-log\.txt)");

for (const auto &file_name : file_names) {
    // std::boolalpha 는 bool 을 0 과 1 대신에 false, true 로 표현하게 해줍니다.
    std::cout << file_name << ": " << std::boolalpha
              << std::regex_match(file_name, re) << '\n';
  }

패턴 일부 뽑아내기

캡처 그룹을 이용하여 주어진 패턴과 일치하는 문자열에서 패턴 일부분을 뽑아낼 수 있다.

smatch를 이용하여 매칭된 문자열을 보관할 수 있다.

smatch는 string을 저장하며 cmatch는 const char*을 보관한다.

std::regex re("[01]{3}-(\\d{3,4})-\\d{4}");
  std::smatch match;  // 매칭된 결과를 string 으로 보관
  for (const auto &number : phone_numbers) {
    if (std::regex_match(number, match, re)) {
      for (size_t i = 0; i < match.size(); i++) {
        std::cout << "Match : " << match[i].str() << std::endl;
      }
      std::cout << "\n";
    }
  }

패턴을 만족하는 문자열 일부를 검색 할 수 있다.

suffix 함수는 원 문자열에서 검색된 패턴 바로 뒤 부터, 이전 문자열의 끝까지에 해당하는 std::sub_match 객체를 리턴한다.

sub_match클래스에는 string으로 변환할 수 있는 캐스팅 연산자가 들어있다.

std::regex re(R"(<div class="sk[\w -]*">\w*</div>)");
  std::smatch match;
  while (std::regex_search(html, match, re)) {
    std::cout << match.str() << '\n';
    html = match.suffix();
  }

regex_iterator

iterator를 이용하여 문자열 검색 가능

regex_iterator는 처음 생성될 때와 ++ 연산자로 증감 될 때마다 regex_search를 통해 문자열을 검색한다.

*연산자를 통해서 역참조하면 현재 검색된 패턴에 대한 match_results 객체를 얻을 수 있다.

std::regex re(R"(<div class="sk[\w -]*">\w*</div>)");
  std::smatch match;

  auto start = std::sregex_iterator(html.begin(), html.end(), re);
  auto end = std::sregex_iterator();

  while (start != end) {
    std::cout << start->str() << std::endl;
    ++start;
  }

regex_replace

원하는 패턴의 문자들을 다른 문자열로 치환

캡쳐 그룹을 이용하여 다른 문자열로 치환 시에 매칭된 문자열을 활용하여 치환할 수 있다.

치환시에 사용하는 $는 back reference라고 부르며 숫자는 캡쳐 그룹의 순서를 나타낸다.

캡쳐 그룹의 순서는 괄호가 열리는 순서대로 $1, $2, …순으로 진행된다.

std::regex re(R"(sk-circle(\d))");
  std::smatch match;

  std::string modified_html = std::regex_replace(html, re, "$1-sk-circle");
  std::cout << modified_html;

string_view

  • 문자열 읽기만 필요할 때 사용
  • C++ 17부터 도입
  • 메모리 할당을 하지 않는다
  • 원본이 사라지면 문제가 생긴다
  • const char*을 매개변수로 사용하면 문자열 길이를 다시 계산해야하지만 string_view는 문자열 길이를 가지고 있다.

STL

vector

  • []연산자, at(): 임의의 위치 원소에 접근
  • begin(): 첫번째 원소를 가리키는 iterator 반환
  • end(): 마지막 원소 한 칸 뒤를 가리키는 iterator 반환
  • push_back(const T& x): 벡터 끝에 원소를 추가한다. 현재의 마지막 원소 뒤에 새로운 원소를 추가하며, 그 원소의 값은 x 의 복사본으로 초기화 된다. capacity 와 벡터 size 가 같다면 내부적으로 재할당이 일어나게 된다. 이 때 이전에 사용되었던 반복자(iterator) , 레퍼런스, 포인터들은 사용할 수 없음 시간복잡도는 amortized O(1)이 된다.
  • insert, erase: 임의의 위치에 원소 삽입 및 제거, 시간복잡도 O(n)

유니폼 초기화를 이용하여 간단하게 초기화

vector<int> inputs{1,2,3,4,5};

vector<vector<int>> inputs{
		vector<int>{3, 6},
		vector<int>{4, 3},
		vector<int>{3, 2},
		vector<int>{1, 3},
		vector<int>{1, 2},
		vector<int>{2, 4},
		vector<int>{5, 2}
	};

list

링크드리스트

  • begin(): 첫번째 원소를 가리키는 iterator 반환
  • end(): 마지막 원소 한 칸 뒤를 가리키는 iterator 반환
  • push_back(const T& x): 마지막 위치에 원소 삽입, 시간복잡도 O(1)
  • insert, erase: 임의의 위치에 원소 삽입 및 제거, 시간복잡도 O(1)

algorithm

  • sort: 정렬
  • stable_sort: 순서를 보장하여 정렬, sort보다 느림
  • partial_sort: 원하는 범위의 정렬된 원소를 원할때 사용하는 정렬함수, 그 외의 원소는 무작위로 나타난다. ex) 0~2번째 순서까지만 정렬되도록
  • remove: 원하는 원소의 뒤에 있는 원소를 앞으로 이동시킨다. erase와 함께 사용하여 원소 제거시에 사용

     vec.erase(remove(vec.begin(), vec.end(), 3), vec.end());
    
  • remove_if: remove와 기본적으로 동일하게 작동하며 조건을 추가할 수 있다.
  • 람다함수 [capture list] (받는 인자) -> 리턴 타입 { 함수 본체 } 또는

    [capture list] (받는 인자) { 함수 본체 }

    • 캡처리스트

      this는 캡처리스트에 표기하지 않아도 암묵적으로 레퍼런스 타입으로 캡쳐된다.

    • [] : 아무것도 캡쳐 안함
    • [&a, b] : a 는 레퍼런스로 캡쳐하고 b 는 (변경 불가능한) 복사본으로 캡쳐
    • [&] : 외부의 모든 변수들을 레퍼런스로 캡쳐
    • [=] : 외부의 모든 변수들을 복사본으로 캡쳐, const 타입으로 캡쳐된다.
  • transform (시작 반복자, 끝 반복자, 결과를 저장할 컨테이너의 시작 반복자, Pred): 전체 혹은 일부를 순회하면서 값을 수정, 모든 원소에 1을 더하는 것과 같은 단순 작업에 사용
  • find: 원하는 원소를 찾을 때 사용, 반복자를 반환, 선형탐색을 하기 때문에 사용하는 컨테이너에 탐색 함수가 있다면 이 함수를 사용하면 느리다.(vector에는 없기때문에 이 함수를 사용)
  • find_if: 조건에 맞는 원소 찾을 때 사용
  • any_of: 조건 중 하나라도 일치하는 원소가 있는지 true, false 반환
  • all_of: 모든 원소가 조건에 일치하는지 true, false반환

예외처리

throw

예외를 던질때 사용

예외로 전달하는 객체는 아무거나 가능하다

std::exception을 상속받은 객체를 만들어 쓰거나 사용하는것이 좋다.

ex) throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");

overflow_error, length_error, runtime_error등

try, catch

try문 내에서 throw 발생시 가장 가까운 catch로 점프

catch(…)

모든 예외 상황을 처리할 때 사용

stack unwinding

throw 호출 후 가장 가까운 catch로 점프할때 스택 상에 정의된 객체들을 소멸시키는 과정 (생성자에서 예외 발생 시 소멸자가 호출되지 않는다, catch안에서 알아서 해제시켜줘야함)

noexcept 키워드

이 키워드가 붙은 함수는 예외가 발생하지 않는다는 것을 컴파일러에게 알려줌(최적화용)

이 키워드가 붙은 함수내에서 throw로 예외 호출시 제대로 처리되지 않고 프로그램이 종료된다 , 소멸자는 기본적으로 noexcept이다.

이동, 우측값 레퍼런스

복사 생략(copy elision)

일반 생성자로 생성한 객체를 복사 생성자로 복사하는 것과 같은 경우 컴파일러 자체에서 복사를 생략하는 최적화가 가능하다.

C++ 표준 상으로는 필수는 아니고 할 수도 있는 기능

lvalue

주소값을 취할 수 있는 값

rvalue

주소값을 취할 수 없는 값, 임시로 사용되고 연산이 끝나면 사라지는 값

우측값 레퍼런스

&&를 사용하여 정의

우측값을 레퍼런스 변수로 사용할 수 있게 하는 기능

레퍼런스 하는 임시 객체가 소멸되지 않도록 한다.

이동생성자

T(T &&target); 와 같은 식으로 사용

객체를 이동시에 객체내부에 존재하는 객체 변수들을 복사하지 않고 주소값만 변경하는 생성자

임시객체의 값을 이용하기 때문에 임시 객체 소멸시 값이 소멸되지 않도록 nullprt을 지정해줘야한다(얕은 복사이기 때문에 원본이 지워지면 같이 사라진다.)

vector와 같은 컨테이너에서 사용시에는 noexcept로 명시하지 않으면 복사생성자를 호출한다.

move함수

좌측값을 우측값으로 캐스팅하는 함수

스마트포인터, RAII(Resource Acquisition Is Initialization)

RAII(Resource Acquisition Is Initialization)

자원의 획득은 초기화한다 자원 관리를 스택에 할당한 객체를 통해 수행

스마트포인터

자원 관리를 스택의 객체(포인터 객체)를 통해 수행하는 것 스마트 포인터를 사용하면 소멸자를 통해 delete가 수행된다.

unique_ptr

특정 객체에 유일한 소유권을 부여하는 포인터 객체

datanew Data()로 생성된 객체의 소유권을 보유하면 delete data만 가능하다

unique_ptr은 복사 생성자가 명시적으로 삭제되어있다.

std::move함수를 통해 소유권을 이전 시킬수 있다.(소유권을 이전한 포인터는 절대 다시 참조하면 안된다.)

std::unique_ptr<>를 통해 생성하거나 auto ptr = std::make_unique(3, 5); 와 같이 make_unique함수를 통해 생성

make_unique 함수가 좀더 효율적(템플릿 인자로 전달된 클래스의 생성자에 인자들을 전달하여 생성하기 때문)

삭제된 함수

사용을 원치 않는 함수를 삭제시키는 방법

삭제된 함수 호출시 컴파일 오류를 발생시킨다.

C++ 11에 추가됨

ex) A(const A& a) = delete;

shared_ptr

여러개의 shared_ptr이 같은 객체를 가리킬 수 있으며 참조 개수가 0이 되면 가리키고 있는 객체를 해제한다.

제어 블록을 동적으로 할당한 후 shared_ptr에 필요한 정보를 공유한다.

make_shared 함수로 생성

enable_shared_from_this

shared_ptr의 인자가 주소 값일 경우 여러개의 제어 블록이 생성될 수 있다.(최대한 지양해야함)

객체 내부에서 자기 자신을 가리키는 shared_ptr을 만들때는 enable_shared_from_this을 사용해야한다.

enable_shared_from_this을 해당 객체가 상속받고 shared_from_this()함수를 이용하여 shared_ptr을 생성하여 사용한다.

shared_from_this()함수는 기존에 정의된 제어 블록이 있을때만 사용가능(shared_ptr이 이미 생성된 이후에 사용가능)

weak_ptr

shared_ptr이 서로 참조하는 순환 참조 문제를 해결하기 위해 사용

shared_ptr과는 달리 참조 횟수를 늘리지 않는다.

weak_ptr이 객체를 가리키고 있더라도 다른 shared_ptr들이 가리키고 있지 않다면 메모리에서 소멸된다.

weak_ptr 자체로는 객체를 참조할 수 없고 shared_ptr로 변환 후에 사용할 수 있다.(가리키고 있는 객체가 이미 소멸되었다면 빈 shared_ptr로 변환된다.)

lock함수를 통해 shared_ptr로 변환

제어 블록에는 약한 참조 개수(weak count)를 기록하며 weak_ptr이 0개일 때 제어 블록이 메모리에서 해제된다.

Callable

()를 붙여서 호출할 수 있는 모든 것

std::function

Callable들을 객체의 형태로 보관할 수 있는 클래스

int some_func1(const std::string& a)
{
  std::cout << "Func1 호출! " << a << std::endl;
  return 0;
}

struct S
{
  void operator()(char c) { std::cout << "Func2 호출! " << c << std::endl; }
};

std::function<int(const std::string&)> f1 = some_func1;
std::function<void(char)> f2 = S();
std::function<void()> f3 = []() { std::cout << "Func3 호출! " << std::endl; };

멤버 함수를 가지는 std::function은 다른 방법을 사용해야한다.(멤버 함수들은 자신을 호출한 객체를 인자로 암묵적으로 받고 있기 때문에)

&연산자를 통해 명시적으로 주소값을 전달해야함

std::function<int(A&)> f1 = &A::some_func;

mem_fn 함수를 이용하여 멤버 객체를 함수 객체로 변환할 수 있다.(자주 사용 안함, 람다함수를 이용하면 동일한 작업 수행 가능)

std::bind

함수 객체 생성 시에 인자를 특정한 것을 지정하는 함수

std::placeholders는 _1부터 _29까지 정의되어 있으며 bind로 생성한 함수 객체 호출시 인자를 전달한다.

인자를 여러개 전달하더라도 지정하지 않은 뒤의 인자는 무시된다.

void add(int x, int y)
{
  std::cout << x << " + " << y << " = " << x + y << std::endl;
}

void subtract(int x, int y)
{
  std::cout << x << " - " << y << " = " << x - y << std::endl;
}

auto add_with_2 = std::bind(add, 2, std::placeholders::_1); add_with_2(3);

auto negate = std::bind(subtract, std::placeholders::_2, std::placeholders::_1);
negate(4, 2);

레퍼런스를 인자로 받는 경우 bind 함수로 인자가 복사되서 전달 되기 때문에 std::ref함수를 이용하여 명시적으로 레퍼런스를 전달해줘야한다.

Thread

사용법

#include <thread> 사용

void func1()
{
	....
}

void worker(vector<int>::iterator start, vector<int>::iterator end,
            int* result) {
	...
}

thread t1(func1);
thread t2(worker, data.begin() + i * 2500, data.begin() + (i + 1) * 2500, &partial_sums[i]);

t1.join();
t2.join();

join함수

해당하는 쓰레드들이 실행을 종료하면 리턴하는 함수

join, detach 되지 않는 쓰레드들의 소멸자가 호출되면 예외가 발생한다.

뮤텍스

한번에 하나의 스레드만 접근 하도록 제어하는 기능을 가진 객체

lock을 호출한 스레드는 해당 객체의 소유권을 갖게 되고 unlock을 호출하기 전까지 다른 스레드는 lock에서 대기한다.

std::mutex m;
m.lock();
// critical section
m.unlock();

lock_guard, unique_lock

뮤텍스를 인자로 받아서 생성하며 생성자에서 뮤텍스를 lock한다.

생성한 스코프 밖으로 나가면 뮤텍스를 unlock한다.

lock_guard은 생성 될때만 lock이 가능하지만 unique_lock은 생성자가 아닌곳에서도 lock이 가능하다.

데드락

서로의 스레드가 상대가 필요로 하는 객체의 lock을 호출한 후 unlock을 호출하지 않아 프로그램이 종료되지 않는 현상

데드락을 피하기 위한 방법

  • 중첩된 Lock 을 사용하는 것을 피해라
  • Lock 을 소유하고 있을 때 유저 코드를 호출하는 것을 피해라
  • Lock 들을 언제나 정해진 순서로 획득해라

sleep

sleep_for 함수는 인자로 전달된 시간 만큼 스레드를 sleep 시키는 함수이다.

std::this_thread::sleep_for(std::chrono::milliseconds(100 * index));

condition_variable

스레드를 특정한 조건을 만족할때 까지 재울수 있는 변수

wait함수를 이용하여 특정 조건까지 쓰레드를 재울 수 있다.

wait함수는 unique_lock을 매개변수로 받으며 조건이 참이 아니면 다른 누가 깨워주기 전까지 계속 sleep 상태로 기다리게 된다.

std::unique_lock<std::mutex> lk(*m);

cv->wait(lk, [&] { return !downloaded_pages->empty() || *num_processed == 25; });

notify 함수

notify_one는 자고 있는 스레드 하나를 깨우는 함수

notify_all는 자고 있는 스레드를 모두 깨우는 함수

atomic

연산 중에 다른 스레드가 개입할 수 없는 원자적인 연산을 보장하는 객체

#include <atomic>
std::atomic<int> counter(0);

memory_order

memory_order_release와 memory_order_accquire를 이용하여 동기화를 수행 가능하다.

  • memory_order_relexed: 메모리에서 읽거나 쓸 경우, 주위의 다른 메모리 접근들과 순서가 바뀌어도 무방
void t1(std::atomic<int>* a, std::atomic<int>* b)
{
  b->store(1, memory_order_relaxed);      // b = 1 (쓰기)
  int x = a->load(memory_order_relaxed);  // x = a (읽기)

  printf("x : %d \n", x);
}
  • memory_order_release: 해당 명령 이전의 모든 메모리 명령들이 해당 명령 이후로 재배치 되는 것을 금지

  • memory_order_accquire: 해당 명령 뒤에 오는 모든 메모리 명령들이 해당 명령 위로 재배치 되는 것을 금지

  • memory_order_acq_rel:acquire 와 release 를 모두 수행, 읽기와 쓰기를 모두 수행하는 명령인 fetch_add 와 같은 함수에서 사용할 수 있음

  • memory_order_seq_cst: 메모리 명령의 순차적 일관성을 보장, 비싼 연산, ++와 같은 후위연산자가 이에 속한다.

std::promise, std::future

어떤 스레드 T를 사용해서 비동기적으로 값을 받아내기 위해 사용하

void worker(std::promise<string>* p) {
  // 약속을 이행하는 모습. 해당 결과는 future 에 들어간다.
  p->set_value("some data");
}

int main()
{
	std::promise<string> p;
	std::future<string> data = p.get_future();
	
	std::thread t(worker, &p);

	data.wait(); // 데이터를 받을때 까지 기다린다.
	cout << "data: " << data.get() << endl;
}

wait함수는 호출하지 않아도 알아서 promise가 future에 객체를 전달할 때 까지 기다린다음에 리턴한다.

future에서 get을 호출하면 설정된 객체가 이동되며 절대로 두 번 호출하면 안된다.

future는 예외도 전달 할 수 있다.

p->set_exception(current_exception());

...

try {
  data.get();
} catch (const std::exception& e) {
  std::cout << "예외 : " << e.what() << std::endl;
}

wait_for 함수는 전달된 시간 만큼 기다렸다가 future_status를 바로 리턴한다.

future_status는 3가지 상태를 가질 수 있다.

future_status::ready, future_status::timeout, future_status::deferred가 있으며 순서대로 future에 값이 설정 됬을 때 나타나는 상태, 지정한 시간이 지났지만 값이 설정되지 않아서 리턴한 상태, 결과값을 계산하는 함수가 채 실행되지 않은 상태로 구분된다.

shared_future

여러 개의 다른 스레드에서 future를 get하기 위해 사용

packaged_task

packaged_task는 전달된 함수를 실행하고 그 함수의 리턴값을 promise에 설정한다.

스레드에 move를 이용하여 전달하면 사용할 수 있다.

비동기적으로 실행된 함수의 결과 값은 추후에 future의 get함수로 받을 수 있다.

std::async

std::async에 어떤 함수를 전달하면 스레드를 알아서 만들어 해당 함수를 비동기적으로 실행하고, 그 결과값을 future에 전달한다.

첫번째 인자로 std::launch::async을 전달하면 함수를 바로 실행하고 결과값을 future에 전달한다.

첫번째 인자로 std::launch::deferred을 전달하면 get 함수가 호출될 때 함수를 실행하고 결과값을 future에 전달한다.(인자 값 생략시에도 이와 같이 작동)

람다함수를 이용하면 깔끔하게 사용 할 수 있다.

유니폼 초기화(Uniform initialization)

{}을 이용하여 초기화

Narrow-conversion이 안되도록 지정된 형식으로만 초기화가 이루어지도록 한다.(생성자의 인자 값이 암시적인 변환이 이루어지지않는다. ex)double- > int)

유니폼 초기화를 이용하면 std::initializer_list을 인자로 받는 생성자를 호출할 수 있다.

std::initializer_list{}의 안에 있는 값들을 지니고 있다.

{}을 이용하여 생성할 때 auto를 타입으로 지정하면 initializer_list 객체가 생성된다.

auto list = {"a", "b", "cc"};과 같이 문자열을 생성하면 initializer_list<std::string>이 아닌 initializer_list<const char*> 이 생성된다. (string 타입으로 지정하기 위해서는 리터럴 연산자를 사용하면 된다)

constexpr

어떠한 식이 상수식 이라고 명시해주는 키워드, 컴파일 타임에 값을 지정한다.

constexpr 함수

constexpr을 함수의 리턴 타입으로 사용하면 해당 함수의 리턴 값을 컴파일 타임 상수로 만들 수 있다.

constexpr 함수에 인자로 컴파일 타임 상수가 아닌 값을 전달하면 그낭 일반 함수처럼 동작한다.

constexpr 함수 내부에서는 아래와 같은 조건에서 컴파일 타임 오류가 발생한다.

  • goto문 사용
  • 예외처리(try문, c++20부터는 예외처리 사용 가능)
  • 리터럴 타입이 아닌 변수의 정의
  • 초기화 되지 않는 변수의 정의
  • 실행 중간에 constexpr이 아닌 함수 호출

constexpr 생성자

사용자가 리터럴 타입을 직접 만들 수 있게하는 생성자

constexpr 생성자의 인자들은 반드시 리터럴 타입이어야만 하고, 해당 클래스는 다른 클래스를 가상 상속 받을 수 없다.

if constexpr

if constexpr이 참이라면 else에 해당하는 문장은 컴파일되지 않고 거짓이라면 else에 해당하는 부분만 컴파일된다.

if constexpr의 조건은 반드시 bool로 타입 변환 될 수 있는 컴파일 타입 상수식만 가능하다.

참고 자료

  • 모두의 코드 C++

Tags:

Categories:

Updated: