내가 보려고 적는 C++ 용어 사전


생각하고 있던 프로젝트를 진행하려고 에디터를 켰더니 마지막으로 C++를 직접 작성한 지 꽤 오랜 시간이 지나버렸다는 것을 깨달았다. 기초적인 것 몇 개를 제외한 나머지 문법이나 자료구조는 제대로 기억이 나질 않아 이번 기회에 여기에 정리하면서 나만의 용어 사전을 만들어보려고 한다.
(사실상 노트이므로 정확하지 않은 글을 적을 수도 있다.)

cplusplus.com의 튜토리얼 문서를 읽어보며 대략적인 틀을 작성하고 보충할 요지가 있는 내용은 따로 조사하여 덧붙이는 형식으로 작성할 것이다.
그리고 대부분이 C++11을 기반으로 작성되어 있으므로 전체적으로 작성한 뒤에도 각 버전에 대한 설명이 추가될 수 있다.

적는 항목의 순서는 내 기준으로 배경 지식 → 문법 → 표준 라이브러리 → 기타 등등 순으로 적을 예정이다.

그리고 모든 내용은 여기에 토막글 형식으로 덧붙이고, 만약에 따로 내용을 정리해야 한다면 글을 분리하고 원래 항목이 있던 자리에 링크를 남길 것이다.

컴파일러 Compiler

C++ 코드를 컴파일하고 구동하는 프로그램으로 빌드할 수 있는 도구

사람이 기계에 직접 0과 1로 프로그래밍 하는 것은 여러모로 에러를 일으키기 쉽다.
그래서 사람이 이해하기 쉬운 고급 언어를 사용하고 컴파일러는 작성된 언어의 코드를 기계어로 번역하여 프로그램으로 변환하므로 양쪽 모두에게 효율적이게 된다.

주로 사용되는 크로스 플랫폼을 지원하는 컴파일러는 GCC, Clang, Code::blocks 등이 있다.

전처리문 Preprocessor directives

컴파일을 하기 위해 코드를 수집하는 과정에서 선언하는 구문, 이 구문에 적힌 내용은 컴파일 타임에 선언한 내용대로 새로 작성되거나 적용된다.

코드 라인의 # Hash sign으로 시작되는 구문을 의미한다.

// 이 전처리 문들은 다른 문과 달리 세미콜론으로 구분짓지 않고 다음 라인으로 넘어가는 문자로 구분된다.
// 만약 라인 하나보다 더 사용할거라면 라인 끝에 백슬래쉬를 넣어 다음 라인에 더 적을 수 있다.

#include <iostream> // 헤더를 불러옴 
#include "파일" // 상대 경로 기준으로 작성할 것, 여기서 파일을 찾지 못하면 <>로 대체되어 검색된다.

#define SPEED_MAX 30000.0f // SPEED_MAX라는 구문을 발견하면 이를 30000.0f로 바꿈
#define getmax(a,b) a>b?a:b // 

#define str(x) #x // #(매개변수 이름)은 그 이름을 문자열로 변환한다.
#define glue(a,b) a ## b // 두 매개변수 사이를 문자열로 변환하고 서로 붙임 (공백 없이 붙임)

#undef SPEED_MAX // 매크로 해제 

// 조건부 포함
#ifdef SPEED_MAX
int a; // SPEED_MAX가 전처리 선언되었다면 이 문장이 삽입된다.
#elif SPEED_MIN // 앞의 if가 실패한 경우
int b;
#else
int c;
#endif  // 조건문 종료 

#ifndef SPEED_MAX // 위의 것과 반대로 이건 선언이 되지 않아야 된다.
int a; // SPEED_MAX가 전처리 선언되지 '않았다면' 이 문장이 삽입된다.
#endif  // 조건문 종료 

#if defined ARRAY_SIZE
// #if ARRAY_SIZE < 50 // 이런 논리 연산자도 사용할 수 있다.
int a;
#elif !defined BUFFER_SIZE
int b;
#else
int c;
#endif  // 조건문 종료 

// 라인 컨트롤
// 보통 에러가 나타난다면 그 파일과 라인을 명시하여 알려주지만 여기서 그 구분에 대한 별명을 정할 수 있다. 
// #line number "이름"
#line 20 "assigning variable"
 // 이제 이 파일의 20번째 줄의 에러는 "assigning variable"으로 출력된다.

// 에러 컨트롤
// #error 에러 이유 // 문자열로 넣지 않는다.
#error A C++ compiler is required!

// 그 외 다양한 플랫폼, 컴파일러 관련 옵션 설정
// 사용하는 컴파일러에서 #pragma를 지원하지 않는 경우는 에러 없이 무시된다.
#pragma 

 

표준 전처리 매크로 Standard predefined macros

https://riptutorial.com/cplusplus/example/4867/predefined-macros (일단 표준만 적음)

C++ 표준에 따라 컴파일러에서 선언된 매크로

이 매크로들은 사용자가 다시 선언하거나 해체하지 말아야 한다.

  • __LINE__ : 해당 매크로가 쓰인 라인 (#line으로 변경 가능)
  • __FILE__ : 해당 매크로가 쓰인 파일 이름 (#line으로 변경 가능)
  • __DATE__ : (Mmm dd yyyy) 형식의 해당 파일이 컴파일 된 날짜 (Mmmstd::asctime()에서 가져옴)
  • __TIME__ : (hh:mm:ss) 형식의 해당 파일이 컴파일 된 시간
  • __cplusplus : 컴파일러가 준수하는 표준 Cpp 버전
    • 199711L: C++98, C++03
    • 201103L: C++11
    • 201402L: C++14

C++11

  • __STDC_HOSTED__ : 운영체제를 통한 호스트(hosted implementation)된 구현 여부 (그리고 모든 표준 라이브러리의 사용 가능 여부), 사용할 수 있으면 1, 그 외에는 0이다.

C++17

  • __STDCPP_DEFAULT_NEW_ALIGNMENT__ : ?

부가적으로 있을 수 있음

  • __STDC__
    C++에서는 그냥 정의만 되어있음.
    C에서 1이면, C 표준에 준수하여 구현되었음을 의미
  • __STDC_VERSION__:
    C++에서는 그냥 정의만 되었음.
    C에서는 사용되는 C 버전을 의미함
    • 199401L: ISO C 1990, Ammendment 1
    • 199901L: ISO C 1999
    • 201112L: ISO C 2011
  • __STDC_MB_MIGHT_NEQ_WC__: 1이면 문자 리터럴에서 멀티바이트 인코딩으로 다른 값으로 바뀔 수 있음을 의미 (확인 필요)
  • __STDC_ISO_10646__: yyyymmL 형식으로 wchar_t 문자 인코딩이 어느 유니코드 표준을 따르는지 확인
  • __STDCPP_STRICT_POINTER_SAFETY__: 1이면 strict pointer safety가 구현됨 (get_pointer_safety)
  • __STDCPP_THREADS__ : 1이면 프로그램이 하나보다 더 많은 스레드를 가질 수 있음을 의미

변수 Variable

하나의 값
단순한 숫자부터 문자, 사용자가 정의한 객체 등을 변수에 담아서 사용할 수 있다.
모든 변수는 최초 선언에 각자의 타입(종류) Type을 선언하며 이 타입을 기준으로 컴퓨터가 값을 읽어낸다.

여기서는 주로 쓰이는 간단한 몇 개의 타입의 예제만 적고 나중에 자료구조 항목에서 이를 자세하게 다룰 예정이다.

타입 정수이름 = 최초 값 (없어도 됨);

int a = 100; // 정수
char a = 'a' // 문자
float a = 3.04 // 소수
bool a = true // 논리 (true, false)

int a, b, c; // 여러 개의 변수를 한 번에 선언
int a (2); // 값이 2인 변수 선언
int a {4}; // 값이 4인 변수 선언

변수들의 선언을 확인했던 컴파일러를 통해 다른 변수에서 타입을 복사하는 또 다른 타입을 선언할 수도 있다.

int a = 573;

// 타입과 값을 복사
auto b = a; // int b = a와 같다.

// 오로지 타입만 복사 
decltype(a) c // int c와 같다.  

일반적으로 전역이나 네임스페이스에 저장되는 변수들을 정적 저장 되었다고 보고 로컬에 저장되는 변수들은 자동 저장되었다고 본다.

정적 저장된 변수는 명시적으로 초기화하지 않아도 값이 0으로 초기화 된다.
자동 저장된 변수의 경우 명시적으로 초기화하지 않으면 이상한 값으로 초기화된다.

  • 리터럴 (Literal): a = 5;
  • 진수에 따른 정수 표현
    0b100 1011 // 2진수
    75 // 10진수
    0113 // 8진수
    0x4b // 16진수
  • 정수 표현의 접미사 : 혼용 가능
    75 // int
    75u // unsigned int
    75l // long
    75ul // unsigned long
    75lu // unsigned long
    • U (u) : 부호 없음 (unsigned)
    • L (l) : long
    • LL (ll) : long long
  • 소수점 표현

    리터럴 (Literal): a = 5;
  • 진수에 따른 정수 표현
    0b100 1011 // 2진수
    75 // 10진수
    0113 // 8진수
    0x4b // 16진수
  • 정수 표현의 접미사 : 혼용 가능
    75 // int
    75u // unsigned int
    75l // long
    75ul // unsigned long
    75lu // unsigned long
    • U (u) : 부호 없음 (unsigned)
    • L (l) : long
    • LL (ll) : long long
  • 소수점 표현
    3.14159
    6.02e23 // 6.02 x 10^23
    1.6e-19 // 1.6 x 10^-19
  • 소수점 표현의 접미사
    3.14159L // long double
    6.02e23f // float
    • F (f) : float
    • L (l) : long double
  • 문자와 문자열 표현
    'z' // 한 글자 ' '
    "Hello world" // 문자열
    ” “
    • 문자 리터럴 접두사 : 문자 범위 지정 (대소문자 구분됨)
      • u : char16_t
      • U : char32_t
      • L : wchar_t
    • 문자열 리터럴 접두사: 문자 접두사 + 인코딩 지정
      • u8 : UTF-8
      • R : 가공되지 않은 문자열
        • 백슬래쉬나 ‘, ” 모두 사용 가능
          R”(string with \backslash)” // 빈 시퀸스
          R”&%$(string with \backslash)&%$” // &%$는 임의의 시퀸스
          “string with \\backslash” // 이것과 같은 문자열이 된다.
  • 문자를 리터럴로 직접 표현
    \20 // 8진수 : 백슬래쉬 이후 값 적기
    \x4A // 16진수 : 백슬래쉬 + x 이후 값 적기


    x = "문자열을 2 \ // 코드 내에서 백슬래쉬로 문자열 여러 줄 쓰기
    줄로 표현하기


    x = "문자열을 2줄로 표현하기// 위의 코드랑 동일한 표현
  • 이스케이프 시퀸스
    • \n : 새로운 줄
    • \r : 캐리지 리턴 (‘엔터’ 키랑 동일)
    • \t : Tab
    • \v: Vertical tab (unused)
    • \’, \”: ‘와 “를 쓸 수 있도록 함
    • \\: 백슬래쉬 (\)
  • 기타 리터럴
    bool foo = true; // or false
    int* p = nullptr;
  • 타입을 정한 상수 표현: 변수 타입 앞에 const 키워드를 붙여 선언

상수 표현 (Constants)

const double pi = 3.1415926;
const char tab = '\t';
  • define을 통한 상수 표현
#define PI 3.14159
#define NEWLINE '\n'

연산자

  • 할당 : =, 순서는 오른쪽에서 왼쪽으로 진행됨
    x = 5;
    x = y;
    y = 2 + (x = 5); // 할당 자체가 값이 될 수 있음 (변수에 할당되면서 값 자체로 할당)
    x = y = z = 5;
  • 산술: +, -, *, /, %
  • 관계와 비교: ==, !=, >, <, >=, <=
  • 논리: !, &&, ||
  • 삼항 연산자: a == b ? true : false;
  • 콤마: , 여러 개의 표현식을 사용 ( , )
a = (b=3, b+2); // 이 경우에는 가장 오른쪽이 먼저 실행됨
  • 비트 연산자: &, |, ^, ~, <<, >>
    • ~ : 비트 반전
  • 명시적 타입 변환: (타입 이름)
    i = (int) f;
    i = int (f);
  • sizeof : 해당 타입이 사용하는 크기 (바이트)
    컴파일 타입에 정해지는 시간 상수를 반환함
  • 연산자 우선순위: 레벨 별로 정해짐

구문 Syntax

하나의 역할을 수행하는 코드의 구문
C++에서는 ; 세미콜론으로 이 구문들을 구분할 수 있다.

세미콜론으로 구분하므로 들여쓰기를 지켜야 하거나 굳이 한 줄 안에 구문을 작성할 필요가 없다.

int a = 0;
int b = 3;


std::cout << 
a + b << std::endl; // 키워드를 모두 적는 선에서 줄은 마음대로 바꿀 수 있다.

함수 Function

코드 루틴의 개념적인 단위

보통 하나의 목적을 달성할 수 있도록 설계된 코드의 집합이다.
우리가 이걸 직접 선언할 수도 있는데 그러기 위해서는 반환 타입과 이름, 매개변수, 그리고 시작과 끝을 선언하고 안에 코드와 반환 return을 작성하면 된다.

반환 타입이 void가 아닌 모든 함수는 값을 반환해야 한다.
또는 void 타입의 함수도 코드의 흐름을 위해 중간에 반환하여 흐름을 제어할 수 있다.

반환타입 함수의 이름(매개변수1, 매개변수2, ...) // 이 형식으로 이어서 매개변수 추가 가능
{ // 함수의 시작
// 함수 내의 코드
} // 함수의 끝

// 예를 들어 두 숫자를 받아서 더하는 함수는..
int add(int a, int b)
{ 
 return a + b; 
}

매개변수는 기본적으로 인자와 동일한 값의 변수를 복사하여 함수 내에서 쓰이게 된다. (로컬 변수)
값을 복사하지 않고 그대로 사용하고자 한다면 포인터래퍼런스를 사용하라.

프로토타입을 명시하지 않으면 메인 함수 뒤에 읽히는 함수들은 사용될 수 없다.

int protofunction (int first, int second);
int protofunction (int, int); // 타입만 적어도 상관은 없다.

함수 오버로드를 통해 같은 함수의 이름를 사용하는 대신에 매개변수를 다르게 설정하여 호출할 때 사용하는 매개변수의 타입에 따라 교체할 수 있도록 할 수 있다. (매개변수가 달라야 한다. 반환 타입만으로는 오버로드 할 수 없다.)

// overloading functions
#include <iostream>
using namespace std;

int operate (int a, int b)
{
  return (a*b);
}

double operate (double a, double b)
{
  return (a/b);
}

int main ()
{
  int x=5,y=2;
  double n=5.0,m=2.0;
  cout << operate (x,y) << '\n';
  cout << operate (n,m) << '\n';
  return 0;
}

Main 함수 Main Function

C++에서 특별한 함수, 프로그램 실행 후 가장 처음으로 진입하는 함수다.
프로그램 빌드 시, 파일의 위치와 관계없이 모든 사용되는 코드들 중에서 무조건 이 함수부터 시작하게 된다.

Main 함수의 프로토타입은 2개가 있다.

int main()
int main (int argc, char *argv[])

매개변수의 쓰임은 아래와 같다.

int argc : 프로그램 실행 시, 명렁어 인수의 갯수를 의미
만약에 우리가 프로그램을 빌드하여 명령 프롬포트에서 실행하고자 한다면 파일이 있는 디렉토리에 들어가 아래와 같이 실행 명령을 내릴 것이다.

example.exe blah lala yolo

이 중 명령어 인수는 blah lala yolo에 해당되며 공백으로 구분된다.
따라서 예제의 경우는 프로그램 실행 후 argc가 3으로 주어진다.

char *argv[] : 프로그램 실행 시. 명령어 인수의 문자열을 의미
argc + 1 사이즈의 배열 안에 각 인수를 가리키는 문자 포인터가 있다.

Main 함수의 반환은 프로그램의 정상 작동하여 제대로 종료되었다면 0을 사용해야 한다. 그 외의 경우는 프로그램이 비정상적인 작동을 하여 종료되었다는 것으로 간주된다. 그 외는 <cstdlib> 참고

주석 Comment

코드에 직접 작성하고 프로그램에서 아무런 행동을 하지 않는 메모
말 그대로다. 프로그래머가 코드의 쓰임이나 해야 할 일 등을 작성하여 개발에 참고할 수 있게 한다.

C++에서는 주석을 사용하는 두 가지의 방법이 있다.

c = a + b; // 해당 줄에서만 주석 처리

/*
여러 줄 이상 주석 처리, 따라서 아래의 구문은 작동하지 않는다.
c = a + b;
*/

식별자 Identifiers

C++에 예약된 키워드들
언어적 차원에서 이 키워드들은 각각의 역할이 있으며 키워드와 동일한 이름의 변수를 선언할 수 없다.

이미 설명한 키워드에 한해서는 여기에 링크를 같이 걸어둘 예정이다.
또는 추후에 식별자들을 각 쓰임에 맞게 분류해둘 수 있다.

alignas, alignof, and, and_eq, asm, auto, bitand, bitor, bool, break, case, catch, char, char16_t, char32_t, class, compl, const, constexpr, const_cast, continue, decltype, default, delete, do, double, dynamic_cast, else, enum, explicit, export, extern, false, float, for, friend, goto, if, inline, int, long, mutable, namespace, new, noexcept, not, not_eq, nullptr, operator, or, or_eq, private, protected, public, register, reinterpret_cast, return, short, signed, sizeof, static, static_assert, static_cast, struct, switch, template, this, thread_local, throw, true, try, typedef, typeid, typename, union, unsigned, using, virtual, void, volatile, wchar_t, while, xor, xor_eq

참고로 C++ 언어는 식별자 외에 변수, 함수 등 모든 요소에서 대소문자를 구분한다!

입출력 Input / Output

흐름 제어 Flow Control

내가 정확히 모르는 부분만 적음.

반복자 구문

  • 범위 기준의 for 반복
    for ( declaration : range ) statement;

    declaration: 일부 변수들을 range의 요소로써 선언한 경우에 사용 가능.
    range: 요소들의 묶음 (배열, 컨테이너, begin, end 함수가 지원되는 타입)
int main ()
{
  string str {"Hello!"};
  for (char c : str)
  {
    cout << "[" << c << "]";
  }
  cout << '\n';
}

반복자 내에서 명시적으로 타입을 정하지 않아도 된다.

for (auto c : str)
  cout << "[" << c << "]";

Goto 명령문

프로그램의 지정된 포인트로 아예 이동, 스코프를 무시하고 스택 프레임 해체가 일어나지 않는다.

ASM처럼 라벨을 지정하고 goto labelname; 문으로 이동할 수 있다.

// goto loop example
#include <iostream>
using namespace std;

int main ()
{
  int n=10;
mylabel:
  cout << n << ", ";
  n--;
  if (n>0) goto mylabel;
  cout << "liftoff!\n";
}

Switch 문

break 문을 case 내에 쓰지 않으면 그대로 순차적으로 진행된다. (SourcePawn하고 가끔 헷갈림)
이는 C 컴파일러가 처음 나온 시기부터 있던 것이 지금까지 형태가 유지된 것이라고 한다.

래퍼런스 Reference

저장된 주소를 참고하는 변수를 의미한다.
타입& 변수이름

&를 붙은 채로 값을 수정한다면 참고하는 ‘주소’를 바꾸고
붙지 않았다면 참고하는 주소의 ‘값’을 수정한다.

키워드: Const

변수 선언에 const를 붙여 이 변수가 변경되지 않을 것임을 명시함
특히, 함수 매개변수에 래퍼런스를 달았을 경우에 사용자 측에서 해당 값이 변경될 지 헷갈릴 수 있으므로 명시해두는 것이 좋다.

키워드: template (탬플릿)

함수 내에 특정 키워드를 호출 시점에 교체할 수 있도록 하는 구문

예를 들어 무언가를 더하는 함수가 있다면 오버로드를 통해 적는다고 해도 동일한 함수를 여러가지로 구현한다면 코드의 가독성이나 유지보수에 힘들어질 수 있다.

따라서 이에 만들어진 template은 함수의 구현 부분이 같다는 가정 하에 특정 키워드를 교체하여 코드 상에서는 하나의 함수지만 컴파일 타임에 새로운 함수로 구현할 수 있게 한다.

template <template-parameters> function-declaration
template-parameters: class 나 typename 또는 임의의 타입이 아닌 존재하는 타입을 넣을 수도 있다.

탬플릿 함수의 호출: name <template-arguments> (function-arguments)

// function template
#include <iostream>
using namespace std;

template <class T> // 여기서 선언된 T를..
T sum (T a, T b)
{
  T result;
  result = a + b;
  return result;
}

int main () {
  int i=5, j=6, k;
  double f=2.0, g=0.5, h;
  k=sum<int>(i,j); // < > 내의 타입으로 교체할 수 있다.
  h=sum<double>(f,g);
  cout << k << '\n';
  cout << h << '\n';
  return 0;
}

물론 2개 이상의 매개변수도 가능하다.

// function templates
#include <iostream>
using namespace std;

template <class T, class U>
bool are_equal (T a, U b)
{
  return (a==b);
}

int main ()
{
  if (are_equal(10,10.0))
    cout << "x and y are equal\n";
  else
    cout << "x and y are not equal\n";
  return 0;
}

임의의 타입이 아닌 이미 있는 타입도 매개변수도 넣을 수 있다.

// template arguments
#include <iostream>
using namespace std;

template <class T, int N>
T fixed_multiply (T val)
{
  return val * N;
}

int main() {
  std::cout << fixed_multiply<int,2>(10) << '\n'; // 컴파임 타임에 정해지는 특성 상, 상수만을 넣어야 한다.
  std::cout << fixed_multiply<int,3>(10) << '\n';
}

또는 탬플릿 인자를 가변으로 받을 수 있다.

#include <iostream>

template <typename T>
void print(T arg) {
  std::cout << arg << std::endl;
}

template <typename T, typename... Types> // ...은 템플릿 파라미터 팩 (0개 이상의 매개변수를 나타냄 )
void print(T arg, Types... args) {
  std::cout << arg << ", ";
  print(args...);

/*
void print(int arg, double arg2, const char* arg3) {
  std::cout << arg << ", ";
  print(arg2, arg3);
}

따라서 각 매개변수에 대한 재귀적인 호출을 하나씩 수행한다.
*/
}

int main() {
  print(1, 3.1, "abc");
  print(1, 2, 3, 4, 5, 6, 7);
}

템플릿 특수화: 특정 탬플릿 매개변수를 받은 경우를 따로 만들 수 있음 (예외 케이스 참고)

template <>
struct Factorial<1> {
  static const int result = 1;
};

template <int X>
struct GCD<X, 0> {
  static const int value = X;
};

인라인 함수 Inline functions

함수를 호출하는 자리에서 그 함수로 가지 않고 그대로 삽입될 함수

일반적인 함수 호출의 부하를 피하기 위해서 단순 연산 같은 짧은 코드의 함수는 아예 함수를 호출하는 자리에 코드를 삽입시킨다. 컴파일 타임이 증가하겠지만 성능 상의 이점이 있으니 마다할 이유가 없다.

또한 컴파일러에서 자체적으로 코드를 최적화하면서 인라인으로 선언하지 않아도 처리하거나 선언해도 처리하지 않을 수 있다.

단순 연산 외에 사용하면 유용할 상황이 있을까?

// 앞에 그냥 inline 키워드만 붙이면 된다.
inline string concatenate (const string& a, const string& b)
{
  return a+b;
}

네임스페이스 Namespaces

광역 변수로 사용할 객체와 함수들의 그룹

이걸로 광역 변수를 사용하다보면 생기는 이름 충돌을 피할 수 있다.
네임스페이스의 이름을 식별자로 사용해 내부에 접근할 수 있다.

myNamespace::a
myNamespace::b
// namespaces
#include <iostream>
using namespace std;

namespace foo
{
  int value() { return 5; }
}

namespace bar
{
  const double pi = 3.1416;
  double value() { return 2*pi; }
}

int main () {
  cout << foo::value() << '\n';
  cout << bar::value() << '\n';
  cout << bar::pi << '\n';
  return 0;
}

나눠서 선언할 수도 있다.

namespace foo { int a; }
namespace bar { int b; }
namespace foo { int c; }

// foo의 경우, a와 c를 내부 변수로 갖게 된다. 

존재하는 네임스페이스의 별칭을 따로 정할 수도 있다.

namespace new_name = current_name;

람다 함수 lambda function

익명 함수

[capture list] (받는 인자) -> 리턴 타입 { 함수 본체 }

캡쳐 목록: 함수 외부의 변수 중에 가져올 목록
– [&] : 외부 모든 변수들을 레퍼런스로 캡쳐
– [=] : 외부 모든 변수들을 복사본으로 캡쳐

키워드: using

선언한 이름을 현재 블록에서 식별자 없이 쓸 수 있도록 함.

네임스페이스의 경우:

using namespace std; // 네임스페이스 std를 지정한다.

std::cout 
cout // 식별자 없이 쓸 수 있게 된다.
// using
#include <iostream>
using namespace std;

namespace first
{
  int x = 5;
  int y = 10;
}

namespace second
{
  double x = 3.1416;
  double y = 2.7183;
}

int main () {
  using first::x; // 멤버 단위로 설정할 수 있다.
  using second::y; // 멤버 단위로 설정할 수 있다.
  cout << x << '\n'; // first::x
  cout << y << '\n'; // second::y
  cout << first::y << '\n';
  cout << second::x << '\n';
  return 0;
}

배열 Array

헷갈리는 것만 적음.

C 언어에서 상속되어 언어에 직접적으로 구현된 기능이다.

배열 초기화 방식:

int baz [5] = { }; // 내부 값을 0으로 초기화한다.

int foo [] = { 16, 2, 77, 40, 12071 }; // 사이즈와 초기값이 이때 정해진다.
int foo[] { 10, 20, 30 };

함수 호출에서 매개변수로 넣은 경우에는 일반적인 변수처럼 값을 복사하는 것이 아닌 주소를 넘긴다. (따라서 래퍼런스가 달려있지 않아도 값이 수정될 수 있다.)
또한 함수 선언 시에 배열 크기를 지정하지 않았다면 호출할 때는 크기 상관없이 넣을 수 있고 지정했다면 그 크기의 배열만 넣을 수 있다.

void printarray (int arg[], int length)
void procedure (int myarray[][3][4])
 // 다차원 배열이라면 맨 앞의 차원만 지정하지 않을 수 있다.

또는 C++에서 자체적으로 구현한 array 컨테이너를 사용할 수 있다.
타입을 기준으로 탬플릿화 되어있고 <array> 헤더로 사용할 수 있다.

이 Array는 기본 배열과 비슷하게 작동하지만 복사가 될 수 있고 멤버 변수에 대한 해체는 포인터를 통해야 한다.

#include <iostream>
#include <array>
using namespace std;

int main()
{
  array<int,3> myarray {10,20,30}; // <타입, 사이즈>

  for (int i=0; i<myarray.size(); ++i)
    ++myarray[i];

  for (int elem : myarray)
    cout << elem << '\n';
}

동적 메모리 Dynamic memory

런타임에만 사용할 변수를 위한 동적 공간 할당

new, new[] 연산자

pointer = new type
pointer = new type [number_of_elements]

연산자를 사용하면 할당된 공간의 주소를 흭득할 수 있다.
배열은 기존의 배열처럼 해당 타입의 변수를 순차적으로 늘인 형태로 할당되고 기존처럼 사용할 수 있게된다. (foo[1], *(foo+1))
기존 배열과의 중요한 차이점은: 컴파일 타임에 고정된 크기만을 사용해야 하는 기존 배열과 달리 동적 할당은 런타임에 원하는 크기로 할당할 수 있게 한다.

프로그램에서 요청한 이런 할당은 시스템이 메모리 힙에 할당하는 것으로 이뤄지지만 아시다시피 이런 리소스는 제한되있어 할당 요청을 모두 받을 수 없을 수 있다.

이에 C++에서는 2가지의 표준 방법으로 할당이 성공했는지 확인할 수 있게 한다.
첫 번째는 예외 처리, 할당이 실패되면 bad_alloc 예외가 발생하여 프로그램 실행이 종료한다.
다른 방법은 예외나 프로그램 실행을 종료하지 않고 포인터 반환을 nullptr을 반환하게 하고 프로그램이 정상적으로 실행하게 한다.
이 방법은 헤더 <new>의 오브젝트인 nothrownew 연산자의 인자로 주면 사용할 수 있다.

int * foo;
foo = new (nothrow) int [5];
if (foo == nullptr) {
  // error assigning memory. Take measures.
}

delete, delete[] 연산자

동적으로 할당된 메모리는 특정한 기간동안 유지되야 하는데 더 이상 필요가 없어졌다면 다른 동적 할당을 위해 메모리를 해체할 수 있다.

delete pointer;
delete[] pointer;

위의 섹션과 함께 선언한다면 아래와 같은 예제가 나온다.

// rememb-o-matic
#include <iostream>
#include <new>
using namespace std;

int main ()
{
  int i,n;
  int * p;
  cout << "How many numbers would you like to type? ";
  cin >> i;
  p= new (nothrow) int[i]; // 에러가 나지 않도록 함
  if (p == nullptr) // 할당 실패 
    cout << "Error: memory could not be allocated";
  else
  {
    for (n=0; n<i; n++)
    {
      cout << "Enter number: ";
      cin >> p[n];
    }
    cout << "You have entered: ";
    for (n=0; n<i; n++)
      cout << p[n] << ", ";
    delete[] p;
  }
  return 0;
}

C 스타일 동적 할당

헤더 <cstdlib> 내의 기존 함수 (malloccallocrealloc, free)를 통한 선언

타입 별칭 Type aliases (typedef / using)

이미 선언된 타입에 대해 다른 이름을 부여한다.
별개의 타입을 만들지는 않는다.

typedef existing_type new_type_name; // C 스타일

typedef char C;
typedef unsigned int WORD;
typedef char * pChar;
typedef char field [50];


using new_type_name = existing_type; // C++ 스타일

using C = char;
using WORD = unsigned int;
using pChar = char *;
using field = char [50];

typedef는 using과는 다르게 탬플릿 영역에서 제한이 있다. (확인 필요)

클래스 Class

우측값 참조 rvalue reference

rvalue는 참조 연산자 (&)를 통해 위치를 참조할 수 없는 모든 값을 뜻한다.
일반적으로 할당 연산자가 있다면 좌측은 보통 식별자를 통한 참조가 되지만 우측은 그럴 수 없다.

이게 어디서 쓰이는 지를 알아보기 위해서 예를 들자면 객체를 반환하는 함수가 있고 그 객체의 값을 따로 어딘가에 담고자 한다면 복사 생성자를 호출할텐데 이 때 매개변수로 준 객체가 해당 스코프에서 벗어나면 삭제되므로 이 값을 따로 복사하여 담는다.

여기서 삭제될 멤버와 써먹을 멤버를 서로 맞바꾸게 한다면 복사할 필요 없이 사용할 수 있지 않을까? 하지만 기본 복사 생성자는 우측값을 받을 수 없다. 따라서 우측값 참조가 가능한 오버로딩을 따로 만들어야 한다.

우측값 참조는 일반적인 참조에 같은 기호를 한번 더 붙여 사용할 수 있다.
이 참조 자체는 이름이 생기게 되어 좌측값으로 변환하게 된다. 우측값으로 취급하고 싶다면 이름이 없는 형태로 사용해야 한다.

A&& variable; // &가 아닌 &&

A& A::operator=(A const& res); // 좌측값을 위한 오버로딩 
A& A::operator=(A const&& res); // 우측값을 위한 오버로딩
{
 // 임시로 생성된 객체이므로 그냥 내용을 맞바꿔 원래 있던 내용을 해제하도록 유도한다.
}

우측값을 위한 오버로딩을 만들었다면 구별을 위해 이 둘을 모두 유지하는 것이 좋다. (암시적 형변환이 발생하여 흐름이 꼬일 수 있다. (확인 필요))

또는 표준 라이브러리의 move 함수를 이용해 이름 있는 값을 우측값으로 변환하여 탬플릿으로 함수를 구성할 수 있다. 이 함수는 우측값 레퍼런스로 바로 반환한다.

사용하는데 두 가지 규칙이 있다.

  • (C++11 이후) & 겹침 문제
    즉, 앞과 뒤와 갯수가 2개여야 우측값 참조로 활용될 수 있다.
    • (A&) & A&
    • (A&) && A&
    • (A&&) & A&
    • (A&&) && A&&
  • (C++11 이후) 특수 템플릿 유추 규칙
template <typename T>
void foo(T&&);

// foo가 좌측값으로 호출된다면 T는 (T&) && -> T&
// 우측값이면 T는 (T&&) && -> T&&로 취급됨

template <typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg) {
  return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}

// 완벽한 전달을 위한 함수
template <class S>
S&& forward(typename remove_reference<S>::type& a) noexcept {
  return static_cast<S&&>(a);
}

template <class T>
typename remove_reference<T>::type&& std::move(T&& a) noexcept {
  typedef typename remove_reference<T>::type&& RvalRef;
  return static_cast<RvalRef>(a);
}

이 때, 오버로드 한 함수에서 예외가 발생 되지 않게 하고 noexcept 키워드로 명시해야 한다. (noexcept의 작동 구조 확인 필요)

예외 Exception

런타임 에러와 같은 예외적인 상황에 ‘핸들러’라고 불리는 특별한 함수로 흐름을 바꾼다. 코드 상에서 try – catch 문을 이용해 다룬다.

// exceptions
#include <iostream>
using namespace std;

int main () {
  try
  {

    // 이 try 블럭 안에서 에러가 발생한다면 catch 블럭으로 흐름이 바뀐다.
    throw 20; // 테스트를 위해 강제로 에러 발생
  }
  catch (int e) // 매개변수로 주어진 int가 있다면 여기로 이동됨
  {

    // throw 문에서 준 매개변수인 20을 받아 출력
    cout << "An exception occurred. Exception Nr. " << e << '\n';
  }

/*
  catch (char a) // 만약 문자로 예외를 발생시켰다면 여기로 이동됨 
  {

  }
  catch (...) // 위의 구문 중에 해당되는 타입이 없다면 상관없이 이쪽으로 이동됨 
  {

  }
*/
  return 0;
}

또한 이 블럭 안에 또다른 try – catch 문을 삽입하여 복잡하게 구성할 수도 있다. 이 경우의 catch 처리는 일단 같은 스코프부터 외부의 catch도 흐름에 따라 확인한다.

try {
  try {
      // code here
  }
  catch (int n) {
      throw;
  }
}
catch (...) {
  cout << "Exception occurred";
}

또한 C++에서 더 이상 사용되지는 않지만 지원은 하고 있는 기능인 Exception specification은 함수 선언에 무슨 에러가 발생될 수 있음을 명시할 수 있다.

double myfunction (char param) throw (int);

위의 예제에서 int가 아닌 매개변수의 예외가 발생된다면 함수에서 핸들러를 찾거나 std::terminate를 호출하지 않고 std::unexpected를 호출한다.

throw 지정자가 아무런 타입이 없는 경우면 무조건 std::unexpected를 호출한다.
지정자 자체가 없는 일반적인 함수라면 std::unexpected를 전혀 호출하지 않고 예외 핸들러를 찾는다.

표준 라이브러리의  <exception> 헤더에 예외 처리를 위한 기반 클래스인  std::exception를 제공한다. 이 클래스의 가상 멤버 함수인 what으로 해당 예외에 대한 설명을 문자열로 덮어쓸 수 있다.

// using standard exceptions
#include <iostream>
#include <exception>
using namespace std;

class myexception: public exception
{
  virtual const char* what() const throw()
  {
    return "My exception happened";
  }
} myex; // 여기서 이미 오브젝트가 선언되었다.

int main () {
  try
  {
    throw myex; // myexception 클래스를 매개변수로 예외 발생 
  }
  catch (exception& e)
  {
    cout << e.what() << '\n';
  }
  return 0;
}

기본적으로 아래와 같은 유형의 예외를 발생시킨다.
이들은 모두 exception 클래스에 상속된 클래스다.

예외설명
bad_allocnew 할당이 실패한 경우
bad_castdynamic_cast가 실패한 경우
bad_exceptionException specification에서 지정되지 않은 예외가 나온 경우?
(thrown by certain dynamic exception specifiers)
bad_typeidtypeid에서 발생됨
bad_function_callfunction 오브젝트에서 발생됨
bad_weak_ptrshared_ptr에서 잘못된 weak_ptr을 받았다면 발생됨

또한 사용자가 상속할 수 있는 일반적인 예외 유형도 제공한다.

logic_error프로그램의 내부 로직에 관한 에러
runtime_error런타임에 생긴 에러

키워드: explicit

함수 생성자 선언에 붙여 암시적 형변환을 통한 생성자 사용을 막음

예를 들어, 함수 매개변수로 원래 생성자의 매개변수를 넣으면 암시적으로 그 생성자를 호출하는 것으로 바뀐다.

이런 예기치 못한 사용을 막기 위해 사용된다.

키워드: mutable

멤버 변수를 변경할 수 없는 const 함수에서도 이 키워드가 선언된 변수는 변경될 수 있다.

mutable int data_;

클래스에 관한 추가 정보

  • 함수의 매개변수에서 자식 클래스를 기반 클래스로 취급(포인터, 래퍼런스)해도 자동으로 자식 클래스의 함수를 호출한다.

    자식 클래스 측에 상속 정보가 남아있기 때문.

    대신 이 경우에는 그 기반 클래스 함수에 vitual 선언이 되어야 한다.
  • 가상 함수의 오버헤드
    컴파일러에서 가상 함수가 하나라도 존재하는 클래스에 대해 가상 함수 테이블을 만듬 (vtable)

    가상 함수를 호출하면 이 테이블을 추적하여 실제 함수를 호출해야 함
  • 다중 상속의 생성자 순서
    간단히 말해 왼쪽에서 오른쪽으로.
    class C : public A, public B // A -> B -> C
  • 다중 상속의 함정
    다중 상속된 클래스가 서로 같은 이름의 변수가 있다면 에러가 생길 수 있음

    상속하는 클래스 앞에 virtual 키워드를 붙이면 다중 상속 받는 경우가 생겨도 한번만 포함될 수 있도록 할 수 있음

템플릿 메타 프로그래밍

  • 메타 프로그래밍: 컴파일 타임에 연산이 끝나는 프로그래밍

표준 템플릿 라이브러리

라이브러리의 종류

  • 임의 타입의 객체를 보관할 수 있는 컨테이너
    • 시퀸스 컨테이너: 배열처럼 객체들을 순차적으로 보관
      ex) vector, list, deque
      • vector:
        index의 타입은 unsigned int: 감소 연산 시, 유의하거나 범위 반복자를 사용할 것

        맨 뒤에 원소를 추가하는 것은 미리 할당된 공간을 다 채울 경우에는 새로 만들어 복사를 수행하므로 O(n)

        RandomAccessIterator 반복자 사용
        반복자를 통해서 사용가능한 함수?
        – insert, erase
      • list: 양방향 연결구조
        vector와는 달리 임의의 위치 원소에 바로 접근할 수 없다.
        대신 원소 중간의 삽입과 제거가 유리하다.

        반복자 연산 시, 증/가감 연산만 가능하다 (++, –)
        이를 BidirectionalIterator라고 부른다. (양방향 반복자)
      • deque:
        임의의 위치에 접근할 수 있고 맨 앞과 뒤의 삽입/삭제가 유리하지만 메모리에 연속적으로 저장되지 않고 원소 중간의 삽입/삭제가 백터만큼 느리다.

        임의의 위치에 접근하기 위해 메모리 상의 위치를 파악하고자 추가적인 용량을 요구함. (거의 8배)

        내부 구조는 여러 개의 원소를 가진 큰 블록 단위로 이뤄져있다.
    • 연관 컨테이너: 키를 바탕으로 대응되는 값을 찾음
      ex) set, map
      + 맵은 셋보다 메모리 사용량이 크다.
      + 정렬되지 않게 하려면 unordered_* 사용
      삽입, 삭제, 찾는 연산이 모두 상수 연산인데 해쉬 함수를 사용하여 원소가 들어갈 위치를 정하게 하기 때문 (따라서 중복 키가 들어가면 비효율적으로 된다)

      원소 갯수가 늘어날수록 해쉬 함수를 바꾸는 경우가 생기는데 (rehash) 이때는 O(n) 시간이 소요된다.

      여기서 직접 만든 타입을 넣고자 한다면 이에 맞는 해
      쉬 함수를 직접 작성해야 한다. (STL 해쉬 함수도 있음)
      해쉬 함수: operator()를 오버로드하고 std::size_t 타입 리턴 (사용되는 위치는 컨테이너 사이즈와 나눈 나머지 값)
      • set: 내부의 원소 존재 유무만을 판별함
        원소의 추가/삭제는 시퀸스 컨테이너보다 빠름 (logN)
        삽입 시, 정렬을 유지하여 맞는 위치에 삽입됨
        또한 중복된 원소를 넣을 경우, 무시된다. (multiset로 허용 가능

        객체에 대한 비교도 가능하지만 비교 연산자가 오버로딩되어 있어야 한다.

        find 함수를 통해 원소 존재 유무 확인

        BidirectionalIterator 사용

        내부 구조가 이진 트리 구조로 이뤄짐
        탬플릿 두번째 인자에 콜백을 지정할 수 있음
      • map: 본질적으로 set과 유사하지만 값까지 가짐
        std::map<std::string, type-name>

        원소를 pair로 넣어야 한다.
        pitcher_list.insert(std::pair<std::string, double>("박세웅", 2.23));
        pitcher_list.insert(std::make_pair("차우찬", 3.04));

        또는 연산자 []로도 넣을 수 있다.
        pitcher_list["야옹"] = 1.11;

        없는 키를 참조할 경우 기본 값으로 추가됨
        마찬가지로, 중복 키를 허용하지 않으므로 나중에 들어온 삽입은 무시됨 (multimap에서 허용됨)

        + 중복을 허용한 경우, [] 연산자는 사용될 수 없다.또한 이 경우에는 중복인 키에 대해 반환이 정해져 있지 않다. (equal_range 함수에서 반환된 반복자를 통해 순회 가능)
  • 컨테이너에 보관된 원소에 접근할 수 있는 반복자
    포인터처럼 사용하여 현재 원소를 확인할 수 있음

    vec.begin() + 2; // 두번째 원소 확인 (포인터 타입 변환)

    – const_iterator: const 속성을 지닌 포인터
    ㄴ cbegin, cend로 얻을 수 있음

    – reverse_iterator: 역순 반복자
    ㄴ rbegin, rend 함수로 얻을 수 있음
  • 반복자를 통해 일련의 작업을 수행하는 알고리즘
    • remove 함수: ForwardIterator (vector, list, set, map 등 사용 가능)
      (Iter start, Iter end, value): value를 뒤로 보냄
      remove_if(Iter start, Iter end, value): 조건에 맞는 value를 뒤로 보냄

      template <typename Iter, typename Pred>
      remove_if(Iter first, Iter last, Pred pred)
      // 함수로 값을 구분할 수 있음

기타 STL 함수들

  • transfrom
transform (시작 반복자, 끝 반복자, 결과를 저장할 컨테이너의 시작 반복자, Pred)

저장될 크기에 유의할 것
  • find: 타입에 find 함수가 없는 경우에만 사용을 권장
    find_if: 특정 조건에 맞는 변수 탐색
  • all_of: 주어진 반복자의 모든 요소가 조건에 맞는 값인지 확인
    any_of: 주어진 반복자에서 하나라도 조건에 맞는 값인지 확인

RAII Reousrce Acquisition Is Initialization

자원 관리를 스택에 할당한 객체를 통해 수행
예외가 발생하여 함수에서 빠져나갈 때, 그 함수의 스택에 정의된 모든 객체의 소멸자가 호출되는 것을 이용 (Stack unwinding)

단, 포인터의 경우는 포인터 ‘객체’로 만들어야 자동으로 해체할 수 있으므로 unique_ptr와 shared_ptr을 사용

  • unique_ptr
    std::unique_ptr<A> pa(new A()); // A* pa = new A()와 같음
    std::make_unique<Foo>(3,5);

포인터에 객체의 유일한 소유권을 보유하여 해당 포인터만 소멸자를 호출할 수 있도록 함 또한 포인터가 소멸될 때에도 가르키던 객체가 소멸되도록 함

할당 연산자 = 를 통한 포인터 복사는 불가, 대신 std::move 함수를 통한 소유권 이전이나 복사 연산자 우회가 가능 (이 때 이전되어 더 이상 안쓰는 댕글링 포인터 참조 시, 런타임 크래쉬 발생)

함수에 인자로 넘기는 경우, 본래의 목적을 준수하기 위해선 본래의 포인터 타입을 넘길 것 (pt.get() 함수로 주소값 흭득)

  • shared_ptr
    std::shared_ptr<A> p1(object); // 동적 할당이 2번 됨
    std::shared_pur<A> p1 = std::make_shared<A>(); // 동적 할당을 한번만 수행

스마트 포인터, 다른 스마트 포인터가 같은 객체를 가르킬 수 있고 그 정보를 저장하고 가져올 수 있다.
또한 소멸자의 호출도 이 참조가 모두 없어졌을 때만 일어나게 된다.

– 참조 갯수 확인: p1.use_count();

– 같은 객체를 가르키는 공유 포인터에는 제어 블록이 서로 공유된다.
단, 인자로 주소값을 전달했다면 제어 블록이 서로 다르게 생성되어 공유가 되질 않으므로 소멸자가 예상하지 못한 타이밍에 호출될 수 있다.
+이는 this 키워드로 전달했어도 마찬가지로 발생되는데 내부의 shared_from_this 멤버 함수에 정의된 제어블록을 대신 사용하여 포인터를 std::enable_shared_from_this를 상속하여 해결 가능. (포인터를 이미 정의한 뒤에 사용해야 함 )

– 만약 같은 객체를 가르키지만 포인터를 서로 가르키는 경우는 참조 갯수를 통한 소멸자 호출에 모순이 생기게 되어 수행할 수 없게 된다.

  • weak_ptr

shared_ptr과는 다르게 참조 개수를 늘리지 않음
– 포인터 자체로는 객체를 참조할 수 없고 lock 함수를 통해 shared_ptr로 변환 후 사용해야 함 (객체가 아직 있다면 shared_ptr, 아니면 nullptr 반환)
– 또한 shared 내의 참조 갯수를 파악하지 못하는 상황에 대비하여 weak count를 별도로 기록하게 됨

Callable

말 그대로 호출할 수 있는 모든 것 (함수 등)
C++에서는 ()를 붙여서 호출할 수 있는 모든 것을 의미

예를 들어 람다 함수를 만들어 특정 변수에 저장하고 이를 호출하는 경우

#include <iostream>

int main() {
  auto f = [](int a, int b) { std::cout << "a + b = " << a + b << std::endl; };
  f(3, 5); // Callable (std::function)
}

해더 <functional>의 std::function는 이런 모든 Callable을 객체 형태로 저장한다.

#include <functional>
#include <iostream>
#include <string>

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; }
};

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

  f1("hello");
  f2('c');
  f3();
}

함수 포인터와는 다르게 Functor (연산자 () 오버로딩 함수)도 제대로 사용할 수 있다는 것이 장점이다.

객체의 멤버 함수의 경우, 자기 자신을 제대로 가르킬 수 있도록 명시해야 한다.

class A {
  int c;

 public:
  A(int c) : c(c) {}
  int some_func() {
    std::cout << "비상수 함수: " << ++c << std::endl;
    return c;
  }

  int some_const_function() const {
    std::cout << "상수 함수: " << c << std::endl;
    return c;
  }

  static void st() {}
};

// 파이썬의 함수 (self) 처럼 자신의 래퍼런스를 첫번째 인자로 받게 한다.
// 멤버 함수는 함수 이름의 주소값 변환이 암시적으로 발생되지 않아서 명시적으로 주소값을 전달해야 한다. 
std::function<int(A&)> f1 = &A::some_func;
std::function<int(const A&)> f2 = &A::some_const_function;

std::mem_fn 함수는 이런 멤버 함수를 바로 변환할 수 있게 한다.

 std::mem_fn(&vector<int>::size);

std::bind는 함수 객체 생성 시의 인자를 특정한 것으로 지정한다.

가변 인자이며 첫 번째 인자에 함수 객체, 그 뒤부터 원래 함수 인자의 인수를 적는다고 보면 편하다.

std::bind(fn_object, param1, param2, ..., param30);

인자가 복사되어 전달되므로 만약 함수의 인자로 래퍼런스를 받을 경우에는 명시적으로 래퍼런스를 전달해야 한다. (std::ref(param1), const의 경우 cref 함수)

#include <functional>
#include <iostream>

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;
}
int main() {
  auto add_with_2 = std::bind(add, 2, std::placeholders::_1); // 첫번째 인자(std::placeholders::_1)를 2로 고정 
  add_with_2(3); // (2 + 3)

  // 두 번째 인자인 4는 무시된다.
  add_with_2(3, 4);

  auto subtract_from_2 = std::bind(subtract, std::placeholders::_1, 2);
  auto negate =
      std::bind(subtract, std::placeholders::_2, std::placeholders::_1); // 첫번째, 두번째 인자를 서로 맞바꿈 

  subtract_from_2(3);  // 3 - 2 를 계산한다.
  negate(4, 2);        // 2 - 4 를 계산한다
}

멀티 스레드

  • 스레드: CPU 코어에서 돌아가는 프로그램 단위
    스레드끼리는 메모리를 공유한다.

프로그램 논리 구조 상에서 연산들 간의 의존 관계가 많을 수록 병렬화가 어려워진다.
또한 흐름은 운영체제가 마음대로 정한다. 스레드가 무조건 다른 코어에 할당된다고 보장할 수 없으며 한 코어 내에서 컨텍스트 스위칭이 일어날 수도 있다.

헤더 <thread>를 이용해 스레드 선언을 할 수 있다.
(리눅스는 컴파일 옵션에 -pthread를 넣어야 함)

// 내 생에 첫 쓰레드
#include <iostream>
#include <thread>
using std::thread;

void func1() {
  for (int i = 0; i < 10; i++) {
    std::cout << "쓰레드 1 작동중! \n";
  }
}

void func2() {
  for (int i = 0; i < 10; i++) {
    std::cout << "쓰레드 2 작동중! \n";
  }
}

void func3() {
  for (int i = 0; i < 10; i++) {
    std::cout << "쓰레드 3 작동중! \n";
  }
}
int main() {
  thread t1(func1);
  thread t2(func2);
  thread t3(func3);

  t1.join();
  t2.join();
  t3.join();
}

thread의 join 함수는 해당 스레드가 실행을 종료하면 반환한다.
detach 함수는 알아서 백그라운드에서 돌아가도록 한다.
만약 둘 중 하나라도 사용하지 않은 상태에서 소멸자가 호출되면 예외가 발생된다.

스레드 생성자의 첫 번째 매개변수는 Callable이며 매개변수가 있는 함수였다면 그 매개변수를 순서대로 나열하면 된다. 또한 해당 함수는 값을 반환할 수 없다!

스레드 함수 내에서 std::this_thread 멤버를 통해 현재 함수의 스레드를 파악할 수 있다.
– this_thread::get_id(): thread ID 반환

스레드 상호 배제: mutex (mutual exclusion), 여러 스레드가 동시에 한 코드에 접근하는 것을 배제함 (= 임계 영역)
– lock() 함수: 해당 뮤텍스를 사용 (하고자 잠금)
– unlock() 함수: 해당 뮤텍스를 사용 끝(잠금 해제)
– try_lock() 함수: lock을 할 수 있다면 true를 반환

또한 뮤텍스를 인자로 받는 std::lock_guard의 생성자에서 뮤텍스를 lock하고 소멸될 때 자동으로 그 뮤텍스를 unlock 하게 할 수 있다.

std::condition_variable 객체는 인자의 unique_lock<std::mutex>를 다음 인자의 함수 내에 어떤 조건이 참이 될 때까지 기다릴지 그 조건을 인자로 전달해야 함 (람다 함수)
– notify_one 함수: 조건이 거짓이라 자고 있는 스레드 중에 하나를 꺠워서 다시 검사하게 함
-nofity_all 함수: 자고 있는 스레드를 모두 깨움 (주로 작업을 모두 마치고 아직 자고 있는 모든 것들을 확인하는데 쓰임)

균일한 초기화 Uniform Initalization

{value1, value2, … }를 통한 초기화를 뜻함

기존의 ()를 통한 생성자와는 다르게 함수 호출과 헷갈리지 않지만 암시적 데이터 손실 형변환을 통한 사용이 불가능.

#include <iostream>

class A {
 public:
  A(int x) { std::cout << "A 의 생성자 호출!" << std::endl; }
};

int main() {
  A a(3.5);  // Narrow-conversion 가능
  A b{3.5};  // Narrow-conversion 불가
}

사용자 정의 타입에서 이런 초기화를 지원하고자 한다면 std::initalizer_list<type>를 통해 사용할 수 있다.

#include <iostream>

class A {
 public:
  A(std::initializer_list<int> l) {
    for (auto itr = l.begin(); itr != l.end(); ++itr) {
      std::cout << *itr << std::endl;
    }
  }
};

int main() { A a = {1, 2, 3, 4, 5}; }
// ()였다면 intializer_list가 생성되지 않는다.
// {}를 이용한 객체가 생성되면 intializer_list를 사용하는 생성자가 최우선으로 고려된다.

auto a = {1};     // std::initializer_list<int>
auto b{1};        // std::initializer_list<int>
auto c = {1, 2};  // std::initializer_list<int>, C17: 타입 선택 기준은 멤버들이 모두 같은 타입인 경우에 정해짐 
auto d{1, 2};     // std::initializer_list<int>, C17: 이건 인자가 하나뿐이라면 인자의 타입으로 추론됨, 그 외에는 에러

// 문자열은 더 특이하다.
auto list = {"a", "b", "cc"}; // initalizer_list<string>이 아닌 <const char*>로 취급됨 

using namespace std::literals;
auto list = {"a"s, "b"s, "c"s}; // 문자열 리터럴 취급을 위해 추가

컴파일 타임 상수 constexpr

말 그대로 컴파일 타임에 식의 값을 정한 상수식을 의미

const와 마찬가지로 상수이므로 수정될 순 없지만 컴파일 타임에 그 값을 정해둬야만 한다. (반면에 const는 컴파일이나 런타임에 초기화될 수 있다. )

함수 리턴 타입에 constexpr을 추가하면 제약 조건에 해당되지 않을 경우에는 컴파일 상수로 만들 수 있다. (아닌 경우는 컴파일 타임 에러, 제대로 된 경우에도 런타임에 사용 가능)

  • goto 문 사용
  • 예외 처리 (C20부터는 가능)
  • 리터럴 타입이 아닌 변수 정의
    컴파일 타임에 정의할 수 있는 타입

    – 기본 소멸자를 가진..
    ㄴ 람다 함수
    ㄴ 사용자 정의 생성/소멸자가 없고 모든 멤버가 public (Arggregate, pair 같은 경우)
    ㄴ constexpr 생성자를 가지고 복사/이동 생성자가 없음
  • 초기화 되지 않는 변수의 정의
  • 실행 중간에 constexpr이 아닌 함수 호출

constexpr 생성자는 constexpr 함수에 적용되는 제약이 모두 적용되고 추가적으로..

  • 생성자의 인자들은 리터럴 타입이어야 함
  • 다른 클래스를 가상 상속 받을 수 없음
#include <iostream>

class Vector {
 public:
  constexpr Vector(int x, int y) : x_(x), y_(y) {}

  constexpr int x() const { return x_; }
  constexpr int y() const { return y_; }

 private:
  int x_;
  int y_;
};

constexpr Vector AddVec(const Vector& v1, const Vector& v2) {
  return {v1.x() + v2.x(), v1.y() + v2.y()};
}

template <int N>
struct A {
  int operator()() { return N; }
};

int main() {
  constexpr Vector v1{1, 2};
  constexpr Vector v2{2, 3};

  // constexpr 객체의 constexpr 멤버 함수는 역시 constexpr!
  A<v1.x()> a;
  std::cout << a() << std::endl;

  // AddVec 역시 constexpr 을 리턴한다.
  A<AddVec(v1, v2).x()> b;
  std::cout << b() << std::endl;
}

컴파일 타임 상수식을 구별하는 if constexpr을 선언할 수도 있다.
bool로 타입 변환될 수 있어야 하며 이 식에서 true가 된다면 else에 해당되는 부분은 컴파일되지 않고 완전히 무시됩니다. (반대로 false라면 else 부분만 컴파일)

키워드: decltype

인자로 넣은 식의 타입으로 치환하는 키워드
컴파일 시에 전체 식이 타입으로 치환된다.

디폴트 생성자가 없는 클래스를 사용한다면 에러가 발생한다.

#include <iostream>

struct A {
  double d;
};

int main() {
  int a = 3;
  decltype(a) b = 2;  // int (prvalue / glvalue)

  int& r_a = a;
  decltype(r_a) r_b = b;  // int& (lvalue / glvalue)

  int&& x = 3;
  decltype(x) y = 2;  // int&& (xvalue / rvalue)

  A* aa;
  decltype(aa->d) dd = 0.1;  // double
}

// 함수 리턴 타입을 정하기
struct A {
  int f() { return 0; }
};

decltype(A().f()) ret_val;  // int ret_val; 이 된다.

// 디폴트 생성자가 없는 타입을 사용한다면 std::declval<type>를 사용하라.
// 이건 런타임에서 사용하면 오류가 발생한다 
// C14부터는 굳이 이럴 필요 없이 리턴타입을 auto로 할 수 있다.

template <typename T>
decltype(std::declval<T>().f()) call_f_and_return(T& t) {
  return t.f();
}

값 카테고리 Value Category

C++의 모든 식에는 항상 타입과 값 카테고리가 존재함.

  • glvalue: 일반화된 좌측값
    • lvalue: 좌측값
      이름이 있는 거의 대부분의 변수
      + 문자열 리터럴
      (멤버 중, enum 값, static이 아닌 멤버 함수 제외)
    • xvalue: 소멸하는 값
  • rvalue: 일반화된 좌측값
    • prvalue: 순수 우측값

type_traits의 메타 키워드

static_assert
컴파일 타임에 식을 확인할 수 있는 키워드다.
따라서 bool 타입의 constexpr만 확인 가능 (그 외에는 에러)


답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다