최근 수정 시각 : 2024-06-04 19:16:14

C++/문법

파일:관련 문서 아이콘.svg   관련 문서: C언어/문법
,
,
,
,
,

파일:상위 문서 아이콘.svg   상위 문서: C++
프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all"<colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C(포인터) · C++(자료형 · 클래스 · 이름공간 · 상수 표현식 · 특성) · C# · Java · Python · Kotlin · MATLAB · SQL · PHP · JavaScript
마크업 문법 HTML · CSS
개념과 용어 함수 · 인라인 함수 · 고차 함수 · 람다식 · 리터럴 · size_t · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 == · === · NaN · null · undefined · 모나드 · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}

1. 개요2. 자료형 (Data Types)3. 함수 (Function)
3.1. 반환 자료형3.2. return3.3. 매개변수
3.3.1. 값으로 전달3.3.2. 좌측값 참조로 전달3.3.3. 우측값 참조로 전달
3.4. inline3.5. noexcept3.6. 함수 오버로딩
4. 클래스 (Class)5. 이름공간 (Namespace)6. 메모리 할당
6.1. 자동 할당6.2. 정적 할당6.3. 동적 할당
7. 언어 연결성 (Language Linkage)8. 저장소 지속요건 (Storage Duration)
8.1. static
8.1.1. static 변수8.1.2. static 함수8.1.3. 클래스 멤버
8.2. extern8.3. thread_local8.4. mutable
9. 상수 표현식 (Constant expression)10. 템플릿 (Template)
10.1. 변수10.2. 함수
10.2.1. 템플릿 인자 추론10.2.2. 완벽한 매개변수 전달
10.3. 클래스
10.3.1. 템플릿 데이터 멤버10.3.2. 템플릿 멤버 함수
10.3.2.1. Deducing this
10.3.3. 템플릿 자료형 멤버
10.4. 제약조건
11. requires12. 특성 (Attribute)

1. 개요

C++의 문법을 간략하게 설명하는 문서이다. C언어하고도 중첩되는 요소들이 많으므로 이 문서를 쉽게 이해하기 위해서는 C언어/문법 문서와 비교하여 참조하는 것이 좋다. 하지만 C언어의 문법에 객체지향 문법만 안다고 해서 C++를 잘 아는건 아니다. 일단 C++11 이후로 추가된 기능이 엄청나게 많기 때문이다. 템플릿 공부도 많이 하는 것이 추천되는 편이다.

2. 자료형 (Data Types)

파일:상세 내용 아이콘.svg   자세한 내용은 C++/문법/자료형 문서
번 문단을
부분을
참고하십시오.

3. 함수 (Function)

[[특성]]
반환 자료형 함수 식별자(매개변수-자료형1 매개변수1, 매개변수-자료형2 매개변수2, ...)
{
...
};

함수 (Function)
함수는 C++에서 실행할 수 있는 코드의 집합이자 하나의 문맥 분기점이다. 함수는 사용자가 정의해두었다가 함수의 이름과 함께 ()[호출]를 붙여 실행할 수 있다. 함수가 실행되면 결과 값을 반환하고 원래 호출 스택으로 되돌아온다. 사용자는 함수를 통해 코드를 분할하면서도 쉽게 정리하고, 재사용할 수 있다. 궁극적으로 더 효율적이며 유지보수가 용이한 프로그램을 만들 수 있다. 함수의 정의에는 기본적으로 반환 자료형, 식별자, 소괄호가 필요하다. 그외에 선택적으로 매개변수를 정의할 수 있고, 평가 지시자(constexprC++11 , constevalC++20 ), 연결성 지시자(static, extern), 예외 사양(nothrow/noexceptC++11 ), 혹은 특성C++11 을 부착할 수도 있다.

3.1. 반환 자료형

ReturnType Indentifier(Params...)
{
return 반환 값;
}
auto Indentifier(Params...)
{
return 반환 값;
}
auto Indentifier(Params...) -> ReturnType
{
return 반환 값;
}
auto Indentifier(Params...) -> decltype(...)
{
return 반환 값;
}
decltype(auto) Indentifier(Params...)
{
return 반환 값;
}
반환 자료형은 함수의 실행 결과로 어떤 종류의 값이 나오는지 나타내는 자료형이다. 함수를 정의할 때는 식별자 앞에 반환 자료형을 넣어야 한다. 이때 직접 자료형을 기입하거나 autoC++11 혹은 decltype(expr))C++11 키워드를 사용할 수 있다. 만약 반환하는 값이 없다면 void를 넣으면 된다. 주의할 점은 자료형 문서에서 말했던 것 처럼 autodecltype(auto)은 스스로 존재할 수 없는 자료형의 별칭이라는 것이다. 그래서 autodecltype(auto)을 이용할 때는 함수의 구현부가 필요하다.

auto를 사용한 경우 함수의 닫는 소괄호 맨 뒤쪽에 ->와 함께 반환 자료형을 적을 수 있다. 자료형의 한정자 때문에 auto를 못써서 자료형을 명시할 필요가 있으나 단번에 자료형을 알기 어려우면, auto Add(T t, U u) -> decltype(t + u) 처럼 작성할 수 있다.

만약 decltype(auto)을 사용하면 반환형을 값 범주(Value Category)까지 완벽하게 보존해서 반환된다. 반환하는 자료형 원본을 T라고 했을 때 반환하는 값이 리터럴같은 prvalueT로 추론된다. 메모리에만 있는 이름없는 객체나 우측 참조자 형변환 따위의 xvalue라면 T&&로 추론된다. 클래스의 필드나 함수 외부의 변수 혹은 전역 변수같이 이름이 있는 lvalue라면 T&로 추론된다.

3.2. return

ReturnType Indentifier(Params...)
{
return 반환 값;
}
void VoidFuntion(Params...)
{
return;
}
void Indentifier(Params...)
{
return VoidFuntion(...);
}
함수를 종료하고 이전 스택으로 되돌아가거나, 값을 반환하는 명령어다. 반환 자료형만 기입해선 안되고 사용자가 함수 내부에서 return 구문을 실행해줘야 한다. 만약 아무것도 반환하지 않는 void 함수라면 실행할 필요는 없다. 이 경우 return;을 실행하면 함수가 즉시 종료된다. 그리고 아무것도 반환하지 않는 함수에서 어떤 아무것도 반환하지 않는 함수 실행 구문을 return하는 건 문제가 되지 않는다. 해당 함수가 실행되고 나서 함수가 즉시 종료된다.

3.3. 매개변수

3.3.1. 값으로 전달

ReturnType Indentifier(Type1 Parameter1, Type2 Parameter2, ...);

ReturnType Indentifier(const Type Parameter);

ReturnType Indentifier(volatile Type Parameter);
값으로 전달 (Pass by value)

<C++ 예제 보기>
#!syntax cpp
import <print>;

void increment1(int x)
{
    ++x;
}

void increment2(volatile int x)
{
    ++x;
}

void increment3(int x, const int y)
{
    x += y;
}

int main()
{
    int a = 100;
    int b = 500;

    increment1(a); // (1) 아무것도 안 함
    increment1(7124820); // (2)

    increment2(a); // (3)
    increment2(a + b); // (4)

    increment3(a, 9058142); // (5)
    increment3(a, b); // (6)

    std::println("a의 값: {}", a) // 100을 출력함
    std::println("b의 값: {}", b) // 500을 출력함
}
함수 내부에서 변수로 사용하기 위해 인자를 값으로 전달하는 것. 여기서 중요한 것은 값으로 전달한 인자는 원본이 사용되지 않고, 함수의 지역변수로써 복사되어서 사용된다. 이를 정확한 용어로는 부패 (Decay)라고 한다. 예제의 함수들은 모두 인자 ab를 매개변수 int x에 복사하면서 원래 한정자를 잃어버린다. 함수 안에서는 매개변수인 x를 증가시키기 때문에 원래 변수 a, b에는 아무런 영향을 주지 못한다.

3.3.2. 좌측값 참조로 전달

파일:상세 내용 아이콘.svg   자세한 내용은 참조에 의한 호출 문서
번 문단을
부분을
참고하십시오.
ReturnType Indentifier(Type& Parameter);

ReturnType Indentifier({{{#DodgerBlue,#CornFlowerBlue 'const'}}} Type& Parameter);

ReturnType Indentifier({{{#DodgerBlue,#CornFlowerBlue 'volatile'}}} Type& Parameter);

ReturnType Indentifier({{{#DodgerBlue,#CornFlowerBlue 'const}}} {{{#DodgerBlue,#CornFlowerBlue volatile'}}} Type& Parameter);
좌측값 참조로 전달 (Pass by lvalue reference)

<C++ 예제 보기>
#!syntax cpp
import <print>;

void increment(int& value)
{ 
    ++value; // 여기서 value는 복사본이 아님
}

int* addressof(int& value)
{
    return &value;
}

double& increment(double& target, const double& plus)
{ 
    return target += plus;
}

int main()
{
    // (1)
    int a = 0;
    increment(a); // a의 값 증가

    // (2)
    const auto& b = addressof(a); // int* const&
    increment(*b); // a의 값 증가

    // (3)
    const int* c = addressof(a);
    //increment(*c); 오류! const int&는 수정할 수 없음

    std::println("a의 값: {}", a) // 2를 출력함

    // (4)
    int d = 400;
    //b = addressof(d); 오류! const& 포인터는 수정할 수 없음
    c = addressof(d); // 문제없음

    // (5)
    double f = 10000.0;
    auto& e = increment(f, 30000.0); // const double&에 숫자 리터럴 전달, e는 f의 참조형으로 f의 값은 40000.0
}
매개변수에 좌측값 참조자를 사용하면 외부에서 인자로 전달된 객체를 그대로 이용할 수 있다. 참조형 매개변수는 일반적인 참조형 변수와 다를 것 없이 lvalue이며 이름만 존재하는 변수다. 불필요한 복사를 막으려면 인자를 참조형으로 받아야 한다. 그리고 & 또는 std::addressof로 원본 변수의 주소를 얻을 수 있다. 자료형 문서에서 언급했듯이 const& 한정자를 사용하면 모든 종류의 값을 받을 수 있다. 가령 500UL 따위의 prvalue, 또는 임시 객체 등의 xvalue도 받을 수 있으므로 const&를 쓰면 문제가 거의 발생하지 않는다 [2]. 그래서 값을 수정하지 않는 함수의 매개변수는 const&를 사용하는 것이 좋다.

3.3.3. 우측값 참조로 전달

ReturnType Indentifier(Type&& Parameter);

ReturnType Indentifier({{{#DodgerBlue,#CornFlowerBlue 'const'}}} Type&& Parameter);

ReturnType Indentifier({{{#DodgerBlue,#CornFlowerBlue 'volatile'}}} Type&& Parameter);

ReturnType Indentifier({{{#DodgerBlue,#CornFlowerBlue 'const}}} {{{#DodgerBlue,#CornFlowerBlue volatile'}}} Type&& Parameter);
우측값 참조로 전달 (Pass by rvalue reference)C++11

<C++ 예제 보기>
#!syntax cpp
import <string>;

struct Position { float x, y, z; };

Position MakePosition_Copy(const Position& pos)
{
    return Position{ pos };
}

Position MakePosition_Move(Position&& pos)
{
    // 매개변수 pos를 그대로 전이
    return std::move(pos);
    // 또는 이동 생성자 사용
    return Position{ std::move(pos) };
    // 또는 수동으로 rvalue 변환
    return Position{ static_cast<Position&&>(pos) };
}

struct Squirrel
{
    std::string myName;
    Position myPosition;
};

void SetPosition_Copy(Squirrel& squirrel, const Position& pos)
{
    squirrel.myPosition = pos;
}

void SetPosition_Move(Squirrel& squirrel, Position&& pos)
{
    squirrel.myPosition = std::move(pos);
    // 또는 수동으로 rvalue 변환
    squirrel.myPosition = static_cast<Position&&>(pos);
}

void Function3_pos_copy(Squirrel& squirrel, const float& x, const float& y, const float& z);
void Function3_pos_move(Squirrel& squirrel, float&& x, float&& y, float&& z);
C++11에서 새로 도입된 인자 전달 방식이다. 자료형 문서를 다시 돌아보자면, &&는 이름이 없는 값이며 메모리에만 존재하는 임시 객체를 의미하거나 혹은 저장되기 전에는 메모리에도 존재하지 않는 리터럴을 표현하기 위한 한정자다. 이런 값들을 rvalue라고 한다. rvalue는 우측값 참조자를 통해 변수가 사용되는 메모리 공간에 복사없이, 마치 처음부터 존재하는 것처럼 처리된다. 복사가 아예 일어나지 않기에 값의 교환을 효율적으로 할 수 있다.

그러나 또 다시 언급하자면 &&는 불안정한 값이기 때문에 반드시 std::move, static_cast<T&&> 따위로 감싸줘야 한다. 불안정하다는 것은 식별자의 존재때문에 일어나는 현상이다. rvalue는 이름이 없어야 하는데, 매개변수에 저장하는 순간 이름이 생기는 것이라서 식별자를 언급하는 순간 lvalue가 되버린다. 만약 감싸지 않으면 함수 안에서는 &&&로, const&&const&로 취급된다. 이러면 주소를 얻을 수 있고 값을 바꿀 수는 있겠지만 성능상의 이득은 사라진다.

또한 한번 사용되면 바로 사라질 값이기에 중복해서 사용할 수 없다. 예를 들어 가변 배열인 표준 라이브러리의 std::vector는 이동된 객체는 크기가 0으로 텅 비어버린다. 또다른 예로는 역시 표준 라이브러리의 std::thread는 운영체제 자원을 사용하기에 복사할 수 없고, 오직 이동만 하도록 구현된다. 이를 이동시키면 내부 리소스가 새로운 객체에 전달되기에 원래 객체는 사용할 수 없다.

3.4. inline

inline ReturnType Indentifier(Params...)
{
return 반환 값;
}
inline void Indentifier(Params...)
{
...
}
inline 함수
inline을 반환 자료형 앞에 붙이면 해당 함수의 쓰임새 부분이 함수의 코드 자체로 대체될 수 있음을 나타낸다 [3]. 이 키워드가 적절하게 쓰이면 함수 호출 오버헤드를 줄이고, 호출 스택도 아낄 수 있어서 좋다. 심지어 최적화 과정에서 아예 함수의 코드를 날리고 결과값만 남길 수도 있다. 그렇지만 대부분의 현대 컴파일러는 알아서 처리를 해준다. 그래서 이 지시자의 의의는 코드를 읽는 다른 개발자들에게 이 함수가 인라이닝이 되도록 설계되었다는 것을 알리면서 컴파일러에게 더 적극적으로 인라이닝을 하라는 지시에 가깝다.

inline 함수는 정의와 선언이 같이 행해져야 한다. 정의가 없는 inline 함수는 컴파일 오류를 발생시킨다.

한편 C++17부터는 이름공간 안의 외부 연결인 inline 함수[4]는 규칙이 생겼다. 컴파일러의 해석 단위(Translation Unit)[헤더]에서 항상 같은 이름과, 같은 명세를 가지게 정의해야 하도록 되었다. 즉 기존의 static 함수와 같은 규칙을 가지게 되었다. 다시 말해서 여러 헤더에서 참조하는 동일한 동일한 이름의 inline 함수는 매개변수나 noexcept 등의 정의가 달라지면 안된다는 뜻이다. 이런 규칙이 제정된 이유는 헤더를 삽입했는데 같은 서명을 가진 함수가 다른 객체로 인식되는 문제가 있기 때문이다. 같은 헤더의 같은 이름공간에서 같은 이름인 함수를 가져왔는데 어떻게 다른 함수일 수 있겠냐는 것이다. 그리고 이로써 inline 함수는 정적 함수는 아니지만, 프로그램 내에서는 똑같은 이름은 곧 동일한 함수며 따라서 항상 같은 메모리 위치에 존재함이 보장되었다.

3.5. noexcept

ReturnType Indentifier(Params...) noexcept;

ReturnType Indentifier(Params...) noexcept(boolean-condition);

ReturnType Indentifier(Params...) noexcept(noexcept(expr));
함수에서 예외를 던지는지 여부를 noexcept를 통해 지정할 수 있다. 이를 통해 컴파일러에게 예외 검사를 배제하도록 지시할 수 있다. 예외를 던질지 말지는 사용자의 자유이지만, 확실하게 오류가 없는 함수라면 noexcept를 놓으면 된다. C++20부터는 noexcept(상수 진리값)를 통해 선택적으로 예외 여부를 지정할 수도 있다. 이를 위해 표준 라이브러리에서는 <type_traits>모듈에서 std::is_nothrow_*같은 명칭의 메타 함수를 제공하고 있다. noexcept안의 표현식은 묵시적으로 평가되기 때문에 복잡한 코드가 달려있다고 성능에 문제는 생기지 않는다.

3.6. 함수 오버로딩

ReturnType Function(Type1 Parameter1, ...);

ReturnType Function(Type2 Parameter2, ...);

ReturnType Function(TypeN ParameterN, ...) noexcept;
함수 과다 적재 (Function Overloading)
C++에서는 사용자가 같은 이름의 함수를 여러개 정의할 수 있다. 함수 오버로딩 혹은 함수 중복 정의 기능은 C언어에서 가장 발전되었다고 볼 수 있는 기능이다. 원래 C언어에서는 모든 함수의 이름이 무조건 달라야 했으나 비슷한 자료형을 인자로 받고 동일한 동작을 수행하는 함수들도 다른 이름으로 구별해야하는 불편함이 있었다. 이름공간의 부재로 인한 식별자 부족 현상도 있었다. 가령 int 또는 long long을 받아 문자열로 바꾸는 함수가 있으면, 그 함수의 이름은 ConvertIntToString(), ConvertLongLongToString() 따위를 사용해야 했었다. C++에서는 ConvertToString() 처럼 같은 양식의 식별자로 통일하고 어디에서나 일관적인 코드 작성이 가능해졌다.

한편 오버로딩을 할 때 반환 자료형만 다르게는 만들지 못한다. 함수 오버로딩은 매개변수의 변형을 기준으로 함수를 구분한다. 즉 매개변수가 달라야 중복 정의를 수행할 수 있다.

4. 클래스 (Class)

#!syntax cpp
import <string>;
import <print>;

class GreetingWorld
{
public:
    GreetingWorld() noexcept
    {
        std::println(myGreet);
    }

protected:
    std::string myGreet = "Hello, world!";
};
파일:상세 내용 아이콘.svg   자세한 내용은 C++/문법/클래스 문서
번 문단을
부분을
참고하십시오.

5. 이름공간 (Namespace)

파일:상세 내용 아이콘.svg   자세한 내용은 C++/문법/이름공간 문서
번 문단을
부분을
참고하십시오.

6. 메모리 할당



파일:CC-white.svg 이 문단의 내용 중 전체 또는 일부는 문서의 r20에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문단의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r20 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

6.1. 자동 할당

<C++ 예제 보기>
#!syntax cpp
#include <utility>
#include <iostream>
int main()
{
    int x = 0;

    // 0을 출력
    std::cout << x << '\n';
    // x의 주소를 출력
    std::cout << std::addressof(x) << '\n';

    return 0;
}
전역 범위가 아닌 함수 안에서 객체(Object)를 생성하면 자동 메모리 할당이다. 여기서 객체는 lvalue(참조)가 아닌 필드를 의미한다. 참고할 점은 함수의 반환값은 굳이 lvalue를 명시하지 않으면 NVRO가 적용돼서 prvalue 또는 xvalue가 되어 메모리 할당이 실제로 일어나지 않는다. 그 함수의 반환값이 실제로 사용될 때만 메모리 할당이 일어나는 것이다.

예제에서는 "int x = 0"가 정수를 메모리에 할당하는것이다. 컴파일러의 구현마다 다르지만 보통 자동 할당은 운영체제 메모리의 스택에 저장된다. 이렇게 만들어진 객체는 함수의 범위(Scope)가 끝나면 회수된다. 참고로 std::array<std::array<T, S>> 같은 다차원 array는 C++17 이후에서만 스스로 해제가 가능하므로 사용한 자료형에 따라 별도로 해제 해줘야 누수가 생기지 않는다.

6.2. 정적 할당

#include <cstdint>
#include <utility>
#include <string_view>

// 전역 변수
float my_float;
/*static*/ int my_st_integer;

// static이어도 문제 없다.
/*static*/ std::int64_t my_i64;
/*static*/ std::int64_t* my_ptr_i64 = &my_i64;

constexpr std::int32_t my_ct_i32 = 40;
constexpr const std::int32_t* my_ct_ptr_i32 = &my_ct_i32;

class MyClass
public:
static constexpr std::string_view Name = "MyClass";
static std::string_view NotCtName1;
static inline std::string_view NotCtName2 = "MyClass";
constinit static inline std::string_view NotCtName3 = "MyClass";
};
int main()
{
/*constexpr*/ MyClass instance1, instance2;
// constexpr 객체가 아니라도 constexpr 정적 필드는 컴파일 시간에 값을 검증할 수 있다.
static_assert(std::addressof(instance1.NotCtName1) == std::addressof(instance2.NotCtName1));
// 정적 필드는 constexpr가 아니더라도 컴파일 시간에 주소를 검증할 수 있다.
static_assert(std::addressof(instance1.NotCtName2) == std::addressof(instance2.NotCtName2));
static_assert(std::addressof(instance1.NotCtName3) == std::addressof(instance2.NotCtName3));
static_assert(sizeof(MyClass) == 1);
// 그런데 소멸자는 호출 가능하다.
// 다만 이렇게 명시적으로 호출하면 소멸자에 정의된 동작이 실행되며 실제로 메모리가 해제되는 시점은 함수의 범위가 끝나는 시점이다.
instance1.~MyClass();
}
}}} }}}프로그램이 시작되면서 고정된 메모리 공간에 할당된다. 코드 내의 정적인 크기를 가진 모든 필드가 해당된다. 가령 전역 변수와 함수 심볼 테이블, 클래스의 정적 필드 등이 있다. 프로그램이 종료될 때 까지 여기의 메모리는 해제할 수 없다. 이때 클래스의 정적 필드는 클래스의 바이트 크기에 영향을 끼치지 못한다. 다만 빈 클래스는 아니므로 바이트 크기는 1이 된다. 그리고 정적 포인터가 가리키는 메모리는 여전히 할당 및 해제가 가능한데, 이는 포인터가 정적 필드인 것이지, 포인터의 대상은 정적이든 아니든 상관없기 때문이다.

6.3. 동적 할당

#!syntax cpp
#include <utility>

int* DanglingMemory()
{
    int x = 0;
    int *ptr = std::addressof(x); // 참조 대상 소실 (Dangling Pointer/Reference)

    return ptr; // 접근 위반 오류 또는 경고. 보안 문제가 발생할 수 있다.
이 코드의 의도는 주소 '0'을 가리키는 포인터를 반환시키는 함수를 만드는것이다. 일단 자동 할당으로 정수 x를 생성하고, 정수 포인터 ptr에게 x의 메모리 주소를 가리키라고 명령하고 ptr를 반환하는 것. 하지만 위에서 서술했듯이, x는 자동 할당으로 만들어진 자료기 때문에 함수의 범위를 벗어나면 x에게 주어진 메모리는 운영체제가 회수한다. 요약하자면 실제로 이 함수를 사용하면 나오는건 0을 가리키는 포인터가 아니라 아무 말도 안 되는 걸 가리키는, 사용하면 안되는 포인터다. 이를 해결하는 방법 중 하나는 동적 메모리 할당 (Dynamic Memory Allocation)이다.
이 역시 컴파일러의 구현마다 다르지만 보통 동적 할당은 스택대신 힙(Heap) 영역에 이루어진다. C++에서는 new 자료형; 형식의 표현식을 사용하면 된다. 조금 소개하자면 T *ptr = new T;와 같이 쓸 수 있다. 위의 예제에서 의도하려 했던 함수를 만들려면 다음과 같이 코드를 짜면 된다.
<C++ 예제 보기>
#!syntax cpp
#include <iostream>
int* CreateMemory()
{
    int *ptr = new int; // 동적 할당
    *ptr = 0;
    return ptr;
}
int main()
{
    int *ptr = CreateMemory();
    // "0" 출력
    std::cout << *ptr << '\n'; 
    delete ptr; // ptr이 가리키는 메모리를 해제한다. 이제 ptr은 사용할 수 없다.
}
헌데 동적 메모리 할당은 자동/정적 할당보다 주의가 요구 된다. 특히 방대한 자료를 다루는 프로그램이라면 메모리를 잘 관리하는 것이 요구된다. 그렇지 않으면 메모리 누수가 일어 나게 된다. 혹은 메모리 초과로 버그가 빈발하거나 먹통이 되버리는 경우가 잦다. 그래서 써먹은 다음에 필요없는 메모리는 반드시 풀어주어야 한다. delete 연산자를 사용하면 new로 동적 할당한 메모리를 해제할 수 있다.
유의할 점은 C언어/문법에서 소개하는 malloc 등으로 C++의 클래스를 건드리면 안된다. 왜냐하면 C++의 객체와 C의 메모리 공간은 엄연히 다른 존재이기 때문이다. new는 C++의 런타임에서 따로 관리되는 메모리를 사용한다. C의 메모리 공간은 C++에서도 존재하며 똑같이 포인터로 표현된다. 반면 객체는 특정 메모리 공간을 특수한 용도로 사용하겠다고 지정한 상태를 말한다. 런타임 오버헤드 때문에 상대적으로 느리기도 하지만. 다시 말해서 new는 바로 해당 메모리 공간에 어떤 클래스가 들어있고, 클래스 인스턴스의 수명(Lifetime)이 관리되며, 곧 생성자와 소멸자를 호출할거라고 선포한 상태란 것이다.
결론적으로 C++의 객체를 C언어 방식으로 할당하면 생성자, 소멸자가 호출되지 않으므로, 정말 특수한 상황이 아니면 malloc, free 등은 쓰지 말아야 한다. 객체 수명 관리가 안되므로 프로그램 동작이 꼬일 위험성이 높다.
반대로 이미 해제한 메모리를 재차 해제하려 하거나(Double-Free), 이미 해제한 메모리에 접근하려 할 경우(Use-After-Free)에도 치명적인 문제가 발생한다. 마지막으로 new로 이미 할당 된 메모리(스택도 포함)에 또 할당 할 수도 있다. 해당 방법으로 스택 메모리에다가 할당한 경우, 따로 해제를 해 주지 않아도 된다. 다만 클래스라면 소멸자는 호출 해 주어야 한다. 그런데 만약 원래 동적 할당했던 필드에 할당했으면 이전에 할당했었던 메모리는 주소만 덮어 씌워지는 것이라 자동으로 해제되지 않는다! 때문에 반드시 이전에 할당했던 메모리를 해제해줘야만 한다. 그러나 이 작업이 여간 힘든 것이 아니므로 중복 할당은 피해야 한다.
마지막으로 백엔드에서 서로 다른 런타임을 사용할 때는 경우 해제(delete)는 할당(new)한 프로그램이 사용하는 런타임 영역 내에서 이루어 져야 한다. GCC가 사용하는 libstdc++, LLVM이 사용하는 libc++, 그리고 MSVC가 사용하는 msvcp는 서로 다른 ABI를 가지고 있으며 따라서 할당 방법도 다르다. libstdc++에서 할당한 포인터를 msvcp가 해제하는 경우 힙 커럽션등의 문제가 생길 수 있다.
<C++ 예제 보기>
#!syntax cpp
#include <new>
#include <memory>
#include <span>
struct MyData
{
    void ExecuteData() const {}
    int a;
    int b;
    int c;
};
struct MyData_Copy
{
    void ExecuteData() const {}

    int a;
    int b;
    int c;
};

struct Packet
{
    [[nodiscard]]
    std::span<const char> Serialize() const noexcept
    {
        return std::span{ reinterpret_cast<const char*>(myData), mySize };
    }

    void* myData;
    size_t mySize;
};

int main()
{
    MyData data{};

    Packet packet1
    {
        .myData = new (std::addressof(data)) MyData{},
        .mySize = sizeof(MyData)
    };

    // acq_data1_packet1는 data를 가리키는 포인터이다.
    // MyData* acq_data1_packet1
    auto acq_data1_packet1 = std::launder(reinterpret_cast<MyData*>(packet1.myData));
    acq_data1_packet1->ExecuteData();

    Packet packet2
    {
        .myData = new MyData_Copy{}, // MyData과 똑같은 구성
        .mySize = sizeof(MyData_Copy)
    };

    // packet2.myData에는 MyData가 아닌 MyData_Copy가 들어있으므로 std::launder는 정의되지 않은 동작에 돌입한다.
    // 따라서 acq_data1_packet2의 값은 알 수 없다.
    auto acq_data1_packet2 = std::launder(reinterpret_cast<MyData*>(packet2.myData));

    Packet packet3
    {
        .myData = new MyData{},
        .mySize = sizeof(MyData)
    };
    delete packet3.myData;

    // packet3.myData는 해제된 메모리 이므로 std::launder는 정의되지 않은 동작에 돌입한다.
    // 따라서 acq_data1_packet3의 값은 알 수 없다.
    auto acq_data1_packet3 = std::launder(reinterpret_cast<MyData*>(packet3.myData));
}
이를 어떻게든 C++안에서 해결하고자 한다면 표준 라이브러리에서 std::launderC++17 , std::start_lifetime_asC++23 , std::start_lifetime_as_arrayC++23 라는 메모리 공간을 객체로 취급하는 함수를 제공한다. 여기서 std::launder는 실제로는 메모리 공간에 객체가 이미 있어야 하므로 조금 더 안정적인 reinterpret_cast를 함수의 모양으로 제공하는 꼴이다.

7. 언어 연결성 (Language Linkage)

파일:상세 내용 아이콘.svg   자세한 내용은 C++/명세 문서
번 문단을
언어 연결성 부분을
참고하십시오.

8. 저장소 지속요건 (Storage Duration)

Storage Duration

8.1. static

정적인 기억공간 (static memory)
static이라는 단어는 정적이라는 뜻이다. 정적이라는 말은 객체가 있는 (가상) 메모리 위치가 (프로그램 내에서는) 변하지 않는다는 뜻이다. 곧 static은 해당 객체가 프로그램 내부에서 영원불멸한 존재임을 보장한다. 변수와 함수에 사용할 수 있다.

8.1.1. static 변수

static 자료형{{{#FFA3D2,#Violet ' 변수 식별자'}}};

static 자료형{{{#FFA3D2,#Violet ' 변수 식별자'}}} = ;

static inline 자료형{{{#FFA3D2,#Violet ' 변수 식별자'}}} = ;

static constinit 자료형{{{#FFA3D2,#Violet ' 변수 식별자'}}};

static constinit 자료형{{{#FFA3D2,#Violet ' 변수 식별자'}}}{};

static constinit 자료형{{{#FFA3D2,#Violet ' 변수 식별자'}}} = ;

static constexpr 자료형{{{#FFA3D2,#Violet ' 변수 식별자'}}} = ;

static inline constexpr 자료형{{{#FFA3D2,#Violet ' 변수 식별자'}}} = ;
정적인 내부 연결 변수 (Static Variable)

<C++ 예제 보기>
#!syntax cpp
namespace NamuWiki
{
    class Squirrel
    {
    public:
        Squirrel(float x, float y, float z);

        float mx, my, mz;
    };

    // (1)
    // NamuWiki::liveEverywhere로 언제든지 사용할 수 있다
    static int liveEverywhere;

    // (2)
    // 오류! 기본 생성자를 호출할 수 없습니다
    static Squirrel allIsNotWell;
}

// 이름 공간과 클래스 안에 있는 정적 변수의 정의는 스코프 밖에, 그리고 헤더라면 별도의 소스 파일에 작성해야 한다.
// 그러나 liveEverywhere이 정말 이때 초기화될지는 확실하지 않다.
int NamuWiki::liveEverywhere = 40;
변수의 자료형 앞에 static, 그리고 선택적으로 inline 지시자를 덧붙일 수 있다. static 변수는 값을 할당해주거나 기본값을 할당할 수 있어야 한다. static 변수를 함수에서 선언했다면 맨 처음에 생성될 때만 값이 할당된다. 만약 static 변수가 클래스의 인스턴스이고, 초기값을 할당하지 않았다면 그 변수는 클래스의 기본 생성자를 호출한다 [6]. 때문에 기본 생성자가 없으면 오류가 발생할 수 있으니 주의해야 한다.

주의해야할 점은 static 변수는 초기화되는 시점이 불분명하다는 것이다. C언어부터 내려오는 유서깊은 문제였으나 지금도 해결되지 않았다. 때문에 분명히 소스 파일에 값을 전달했건만 그 값을 못 읽는 경우가 있다는 것이다. C++11에서 이를 대체하기 위한 inline constexpr와 장황한 static inline constexpr 변수가 등장했으나 이는 컴파일 상수가 될 수 있는 객체만 허용하며 객체 초기화에 () 를 쓰면 함수 정의와 헷갈리는 문제가 있었다. C++20에는 변수 초기화 전용으로 constinit이 추가됐다.

8.1.2. static 함수

[[특성]]
static 반환 자료형{{{#FD8C6E ' 함수 식별자'}}}(매개변수-자료형1 매개변수1, 매개변수-자료형2 매개변수2, ...)
{
...
}
정적인 내부 연결 함수 (Static Function)

<C++ 예제 보기>
#!syntax cpp
import <string_view>;

// 구현 내용이 없으면 링크 오류가 발생한다.
static char GetFirstCharacter(std::string_view str);

// 헤더를 여러번 삽입하더라도 GetFirstCharacter는 언제나 동일한 함수임이 보장된다.
// 내부 연결 객체는 현재 이름공간에 유일한 존재로 남으므로 식별자의 중복 선언 문제가 발생하지 않는다.
static char GetFirstCharacter(std::string_view str)
{
    return str.front();
}
함수의 반환 자료형 앞에 static, 그리고 선택적으로 inline 지시자를 덧붙여 해당 함수가 정적인 내부 연결을 가진 함수임을 나타낸다. 원래 C의 소스 구조에서는 여러 곳에서 중복 삽입될 수 있는 헤더는 중복된 객체 링킹 때문에 오직 전방위 선언만을 사용해야했다. 이를 해결하기 위해 static 또는 extern을 소스 파일이 아니라 헤더 파일에서 함수와 변수를 정의하기 위해 사용해왔다. 다만 이젠 이름공간을 사용할 수 있기 때문에 의미가 퇴색된 바 있다. 그리고 C++20에서 소개된 모듈에선 static 함수는 모듈 밖으로 내보낼 수 없다! 모듈 밖으로 객체를 내보내는 export 구문은 외부 연결인데 static 함수는 내부 연결이라서 링크 오류가 발생한다.

static 함수는 정적인 메모리의 특징과 함께 내부 연결이라서 전방위 선언만 할 수 없고 inline처럼 구현도 해줘야 한다. 그런데 inline은 사용자가 함수의 정의를 알아서 고쳐야 했지만 static은 그 자체로 함수의 특징을 강제한다. 그래서 현재 해석 단위[헤더]에서는 이름이 같은 함수이면 무조건 동일한 함수를 의미하고, 프로그램 내내 일정한 메모리 주소에 위치함을 알 수 있다. 그렇기에 static 함수가 헤더에 정의되어 있을 때에는 여러 곳에서 헤더를 삽입해도 링크 오류없이 사용할 수 있다

8.1.3. 클래스 멤버

<C++ 예제 보기>
#!syntax cpp
class GameObject
{
public:
    static inline const int firstId = 1000;
    static const std::string DefaultName;

    static GameObject* CreateObject()
    {
        // objectId는 단 한번 할당된다
        static int objectId = firstId;

        return new GameObject{ .myID = objectId++, .myName = DefaultName };
    }

    int myID;
    std::string myName;
};

template<typename crtp>
class ISingleton
{
public:
    static void SetInstance(crtp* inst)
    {
        if (Instance == nullptr)
        {
            crtp = inst;
        }
        else
        {
            throw "Singleton error!";
        }
    }

    [[nodiscard]]
    static crtp* GetInstance() noexcept
    {
        return Instance;
    }

protected:
    static crtp* Instance = nullptr;
}

std::string GameObject::DefaultName = "GameObject";
정적인 외부 연결 멤버 (Static Class Member)
클래스의 정적인 멤버는 외부 연결로 취급된다. 이 규칙이 없으면 헤더와 모듈C++20 에서 정적 멤버를 사용할 수 없을 것이다. 정적 멤버의 종류는 usingtypedef의 자료형 별칭, 정적 데이터 멤버, 정적 멤버 함수가 있다.

클래스 내의 정적 멤버는 클래스 인스턴스를 만들 필요도 없이 클래스 이름만으로 사용할 수 있다. C++에는 메타클래스가 없으므로 클래스 자체는 메모리에 정적으로 고정된 객체다. 그리고 클래스의 고정된 메모리 주소로부터 상대적 위치만 저장해서 멤버를 구별한다. 그래서 C++의 클래스는 정적 멤버가 아니더라도 일단 MyPhone::callNumber와 같이 이름만이라도 접근할 수는 있다. 그러나 컴파일 오류가 발생하거나 메모리 접근 위반 0x00000016 참조! 따위의 런타임 오류가 발생할 것이다. 여기서 0x00000016이 클래스 MyPhone에 대한 필드 callNumber의 상대적 주소다. 여기서 정적 멤버는 고정된 클래스 메모리 주소 + 고정된 멤버 메모리 주소가 합쳐져 프로그램 내내 일정한 메모리 위치에 존재한다. 때문에 프로그램 안에서는 문맥 상관없이 모두가 참조할 수 있는 객체가 된다. 그런데 이게 외부 연결의 특징을 띄기에 클래스의 정적 멤버가 외부 연결로 취급되는 것이다.

static 멤버가 가장 잘 활용되는 곳은 싱글톤 패턴 클래스다. 싱글톤은 프로그램에서 클래스에 대해 유일하게 존재하는 인스턴스를 구현하는 패턴인데, static이 정확하게 이 목적에 부합한다.

8.2. extern

<C++ 예제 보기>
#!syntax cpp
extern class GameObject;

정적인 외부 연결 (external Linkage)
객체 선언의 자료형 앞에 extern, 그리고 선택적으로 inline 지시자를 덧붙일 수 있다.

extern은 객체가 정적인 외부 연결이라고 나타낼 수 있다. 그런데 static과는 반대되는 기능이 아님을 명심해야 한다. extern 변수, extern 함수도 정적인 객체이며 단지 정의를 외부에서 가져올 수 있다는 뜻이다. extern도 정적인 이유는 외부 인터페이스에서 객체(변수, 함수)에 접근했을 때, 해당 객체가 이전에 같은 이름으로 접근했던 바로 그 존재임이 확실하지 않기 때문이다. 정적인 객체는 한번 할당되면 다시는 새로운 주소가 할당되지 않으므로, 외부 연결임에도 동일한 객체를 유지하는 목적이 있다. 때문에 C++17에서 static 대신 내부 연결만을 적용하기 위해 이름없는 이름공간이 도입되었는데, extern은 외부 연결만 적용하는 키워드가 없다. 내부 코드가 바뀔 수 있는 위험성이 있으므로.

두번째 기능도 있는데, 템플릿 실체화(Template Instantiation) 기능이다. 템플릿의 구현을 해당 소스에서만 컴파일하도록 지시한다. 템플릿은 모든 자료형 후보에 대해 컴파일을 시도하는데, 이러면 컴파일 시간이 기하급수적으로 늘어날 수 있다. 이때 원하는 특수화 후보를 명시하면 중복되는 컴파일 시도를 줄일 수 있다.

세번째로 C와 C++를 언어 전환하는 기능도 있다. extern "C", extern "C++" [8]과 같이 사용한다. 기본적으로 C++의 모든 객체에는 extern "C++"이 적용된다. 모든 이름공간, 클래스와 변수 앞에 보이지 않는 extern "C++"이 붙어있다고 생각하면 된다. 그리고 extern "C"를 사용하면 함수 오버로딩 금지 등 C언어의 규칙을 따로 적용할 수 있다 [9].
해당 키워드를 가장 많이 쓰는 경우는 ABI가 호환되는 라이브러리를 만들 때로, 예를 들어 void* myFunction(void* ptr, const void* cptr, int x)의 경우, MSVC는 ?myFunction@@YAPEAXPEAXPEBXH@Z, clang/GCC는 _Z10myFunctionPvPKvi로 심볼이 변환되게 되는데 (Mangling) extern "C"선언을 통해 본 함수명이자 C 방식인 myFunction이라는 심볼명을 그대로 사용하여 dlsym()이나 다른 프로그래밍 언어에서 C++로 작성된 코드를 불러와 사용하는 것이 가능해진다.

8.3. thread_local

<C++ 예제 보기>
#!syntax cpp
import <vector>;
import <thread>;
import <chrono>;
import <print>;

// 전역 범위에 선언되어 있지만, 실제로는 스레드 단위 지역 변수다.
thread_local size_t threadID;
thread_local size_t threadCount = 0;

void Watcher(size_t id)
{
    // 보이지 않는 threadID, threadCount 지역 변수가 선언되어 있다.
    threadID = id;

    using namespace std::chrono_literals;

    while (true)
    {
        if (::rand() % 10 == 0)
        {
            std::println("스레드 ID {}에서 {}번째 보고", threadID, ++threadCount);
        }

        std::this_thread::sleep_for(1s);
    }
}

int main()
{
    std::vector<std::jthread> myThreads{};
    myThreads.reserve(4);

    for (size_t i = 0; i < 4; ++i)
    {
        myThreads.emplace_back(Watcher, i);
    }

    while (true)
    {
        std::this_thread::yield();
    }
}
스레드 연결 (Thread Linkage)
이름공간[10], 클래스에서 변수 선언에 사용할 수 있다. 전역 변수 선언에 사용하면 이 순간부터 프로그램에서 사용한 모든 스레드에 해당 변수가 선언되게 된다. 참고로 내외부 연결성에 관여하지는 않으므로, static 이나 extern과 조합해서 쓸 수 있다.

8.4. mutable

<C++ 예제 보기>
#!syntax cpp
import <atomic>;
import <vector>;
import <thread>;
import <print>;

class SpinLock
{
public:
    constexpr SpinLock() noexcept = default;
    ~SpinLock() noexcept = default;

    // const 메서드이지만 myState를 수정하고 있다.
    void Lock(std::memory_order model = std::memory_order::memory_order_acquire) const volatile noexcept
    {
        while (!TryLock(model));
    }

    bool TryLock(std::memory_order model = std::memory_order::memory_order_relaxed) const volatile noexcept
    {
        return !mySwitch.test_and_set(model);
    }

    void Unlock(std::memory_order model = std::memory_order::memory_order_release) const volatile noexcept
    {
        mySwitch.clear(model);
    }

    [[nodiscard]] bool IsLocked() const noexcept
    {
        return mySwitch.test(std::memory_order::memory_order_relaxed);
    }

    SpinLock(const SpinLock&) = delete;
    SpinLock& operator=(const SpinLock&) = delete;

private:
    mutable volatile std::atomic_flag mySwitch;
};

// const여도 아무 문제 없다.
const SpinLock globalLock{};
size_t globalCounter = 0;

void Incrementor(size_t max) noexcept
{
    for (size_t i = 0; i < max; ++i)
    {
        globalLock.Lock();
        globalCounter++;
        globalLock.Unlock();
    }
}

int main()
{
    constexpr size_t target = 1000'0000'0000'0000;
    constexpr size_t thrd_count = 10;
    constexpr size_t thrd_workout = target / thrd_count;

    std::vector<std::thread> myThreads{};
    myThreads.reserve(thrd_count);

    for (size_t i = 0; i < thrd_count; ++i)
    {
        myThreads.emplace(Incrementor, thrd_workout);
    }

    for (auto& th : myThreads)
    {
        th.join();
    }

    std::println("결과: {}", globalCounter); // 1000000000000000

    return 0;
}
수정 가능 (Mutable)
클래스의 필드에 사용할 수 있다. const 한정자와는 같이 적용할 수 없다. 이 요건이 적용된 필드는 const 인스턴스, const 메서드에서도 수정할 수 있다. 상단의 예제는 대표적으로 쓰이는 스핀락의 구현이다.

9. 상수 표현식 (Constant expression)

C++11 부터 도입된 상수 표현식, 또는 상수식은 실제 코드가 실행되는 사용자 시점(런타임)이 아니라 컴파일 시점으로 코드의 평가를 앞당길 수 있는 획기적인 기능이다. C++의 킬러 요소라고 말할 수 있는 핵심 기능이다 [11]. 바이너리가 생성되는 컴파일 시점에 실행 결과가 결정되기 때문에 아무런 평가 과정도 컴파일 이후에 남지 않는다. 곧 프로그램 이용자의 실행 시점에는 코드 실행 시간이 0이 되도록 최적화된다.
파일:상세 내용 아이콘.svg   자세한 내용은 C++/문법/상수 표현식 문서
번 문단을
부분을
참고하십시오.

10. 템플릿 (Template)

자료형, 함수, 클래스를 모두 읽고 오는 것을 추천한다.

10.1. 변수

10.2. 함수

#!syntax cpp
template<typename... 템플릿 매개변수>
자료형 또는 auto
함수의 이름(매개 변수...)
{
    ...;
}
템플릿 함수의 개형
#!syntax cpp
template<typename... 템플릿 매개변수> // (선택)
[[특성1, 특성2, ...]] // (선택)
static // (선택)
inline // (선택)
constexpr 또는 consteval // (선택)
자료형 또는 auto
[[특성1, 특성2, ...]] // (선택)
함수의 이름(매개 변수...)
noexcept 또는 noexcept(조건) // (선택)
-> 후속 반환형 // (선택)
{
    실행문...; // (선택)
}
문맥 지시자, 특성, 예외 사양, 템플릿까지 모두 사용한 가장 일반적인 함수의 개형
<C++ 예제 보기>
#!syntax cpp
// 매개 변수가 있고 반환값은 없는 함수
template<typename T>
void SetID(const T& obj, unsigned long long id)
{
    obj.id = id;
}

// 사용자 정의 noexcept 명세를 사용하는 함수
inline constexpr size_t MySize = 10;
int MyBuffer[MySize]{};

template<size_t Index>
constexpr int& Set(const int& value) noexcept(Index < MySize) // Index가 MySize보다 작으면 오류가 없다.
{
    // 그러나 예외를 잡아내는 코드를 생성하지 않는다는 거지, 예외가 발생하지 않도록 하는 건 아니다.
    // 여전히 Index가 MySize 이상이면 오류가 발생한다.
    // 그냥 noexcept로 지정하면, 메모리 접근 위반이 발생했을때 예외 알림 대신 프로그램이 종료된다.
    return MyBuffer[Index] = value;
}

// 후속 반환형을 사용하는 함수
// 제약조건, noexcept 명세, 후속 반환형 사용
template<typename T, size_t Size>
    requires std::copyable<T> // <concept>
constexpr auto CreateArray(const T& value)
    noexcept(std::is_nothrow_copy_constructible_v<T>) // <type_traits>
    -> std::array<T, Size>
{
    // <array>
    std::array<T, size> result{};
    // <ranges>
    std::ranges::fill(result, value);

    return result; // Return Value Optimization 적용
}

10.2.1. 템플릿 인자 추론

<C++ 예제 보기>
#!syntax cpp
import <string>;
import <print>;

template<typename T>
void increment1(T x)
{
    ++x;
}

void increment2(auto x)
{
    ++x; // 전위 증가 연산자를 사용할 수 없으면 예외 발생
}

template<typename T>
void increment3(T lhs, T rhs)
{
    lhs += rhs;
}

template<typename T>
void increment4(T lhs, const T* rhs)
{
    lhs += *rhs;
}

void increment5(auto lhs, auto rhs)
{
    lhs += rhs;
}

int main()
{
    int a = 100;
    long long b = 500;
    const int c = 900;
    int& d = a;

    increment1(a); // 아무것도 안 함 (1)
    increment1(510942633); // (2)
    increment1(b); // (3)
    increment1(d); // (4) d는 a의 참조 변수이지만 &가 부패해서 사라진다
    increment1('B'); // (5)
    increment1("namu"); // 오류! 문자열은 더할 수 없습니다

    increment2(a); // (4)
    increment2(a + b); // (5) 값에 의한 전달은 prvalue도 전달할 수 있다
    increment2('B'); // (6)
    increment2("wiki"); // 오류! 문자열은 더할 수 없습니다

    increment3(a, 1058142); // (7)
    increment3(a, c); // (8) 인자의 const는 매개변수의 auto에 영향을 끼치지 못한다
    increment3(a, b); // 오류! 전달된 두 매개변수 T의 자료형이 서로 다릅니다

    increment4(a, &c); // (9) 포인터(주소)는 glvalue만이 가질 수 있다. glvalue는 lvalue라서 모든 한정자를 반드시 유지한다
    increment4(a, &b); // 오류! 전달된 두 매개변수 T의 자료형이 서로 다릅니다
    increment4(a, &d); // 오류! 포인터와 인자의 const 한정자가 일치하지 않습니다

    increment5(a, b); // (10)
    increment5(d, c); // (11) d는 참조형이지만 auto에서 &가 부패해서 사라진다
    increment5(c, d); // (12)
    increment5(d, b); // (13)
    increment5(std::string{ "Namu" }, std::string{ "Wiki" }); // (14)

    std::println("a의 값: {}", a) // 100
    std::println("b의 값: {}", b) // 500
    std::println("c의 값: {}", c) // 900
    std::println("d의 값: {}", d) // 100 (a의 참조형)

    return 0;
}
Template Argument Deduction
여기서 increment1 함수와 increment2 함수는 서로 같은 의미를 가진다. 이게 중요한 이유는 바로 템플릿과 auto는 본질적으로 같은 뜻이라는 걸 내포하기 때문이다.
상단의 부패를 계속 설명하자면 여기서도 유의할 점이 있다. 원래 auto를 쓰면 &, &&, C++ 배열[]이 모두 증발한 자료형이 추론된다. 함수의 매개변수에 사용되는 auto는 바로 곧 템플릿이며, 각각이 다른 자료형으로 추론되는 템플릿일 뿐이다. 가령 void Function(auto value)에서 매개변수 value는 값에 의한 전달을 수행한다. 그렇기에 auto나 템플릿이나 원래의 한정자가 부패하는 것이다. 이렇게 해야 함수 내부의 값과 외부의 값을 분리하고 의도치 않은 동작을 막을 수 있다. C++에서 의도하지 않은 동작은 모두 일어나서는 안되는 일이다. 사용자가 직접 &, const&, && * 따위의 한정자를 지정하지 않으면 컴파일러는 무조건 값에 의한 전달을 수행한다. 이를 막으려면 자료형 문서에서 설명한 것 처럼 T&& 또는 auto&&로 완벽한 자료형을 얻어야 한다. 이를 함수에서 사용하는 방법은 다음 단락에서 설명한다.

10.2.2. 완벽한 매개변수 전달

<C++ 예제 보기>
#!syntax cpp
import <type_traits>;

void ValueFunction(auto value);
void LvalueFunction(auto& value);
void RvalueFunction(auto&& value);

auto&& Function4_forwarding(auto&& value)
{
    // 복사, &, &&, []가 모두 사라짐 (Decay)
    // 복사할 수 없는 값이라면 오류 발생함
    return value;

    // std::move는 lvalue를 보존하지 않기 때문에 문제가 생긴다.
    // value가 glvalue
    //  T&: T&&
    //  const T&: lvalue는 const T&, xvalue는 const T&&
    // value가 rvalue
    //  T&& - T&&
    //  const T&& - const T&&
    return std::move(value);
}

int main()
{
    const long A = 132435;

    ValueFunction(A); // value는 long
    ValueFunction(std::move(A)); // value는 long
    ValueFunction(8000); // 리터럴 value는 int

    LvalueFunction(A); // value&는 const long&
    LvalueFunction(std::move(A)); // value&는 const long&
    LvalueFunction(8000); // 오류! 리터럴은 lvalue에 대입할 수 없음

    RvalueFunction(A); // value&&는 const long&
    RvalueFunction(std::move(A)); // value&&는 const long&&
    RvalueFunction(8000); // 리터럴 value&&는 int&&
}
완벽한 전달 (Perfect Forwarding)
auto는 인자의 자료형을 썩히고(Decay), * 혹은 순수한 자료형만 보존한다. 즉 const, volatile, &, &&는 무시하고 값으로 전달을 시행한다. 왜냐하면 썩힌다는 것은 최소한의 의미만 남기고 자료형을 날린다는 것인데, 단일 const, volatile은 함수에 전달된 이상 아무 의미가 없기 때문이다. 참조형이 아니라면 그게 상수던 휘발성이던 값으로 전달될 것이고, 그럼 복사가 되든 이동이 되든지 간에, 인자로 전달된 순간부터는 함수 안에서 밖으로 영향을 끼치지 못한다. 사용자 단에서도 const는 단지 코딩에서 실수를 줄이거나 모호함을 줄이기 위해 구태여 붙이는 한정자이지, 인자로 전달됐던 원본 값이랑은 전혀 연관이 없는 변수가 된다. const, volatile, &, &&은 서로 보완하지 않으면 함수 안에서는 아무 의미를 갖지 못한다.

그래서 사용자가 auto&로 지정하면 &에 의존하는 모든 한정자가 딸려나온다. 굳이 const volatile을 붙이지 않아도 말이다. 그러나 const또는 volatile가 없는 auto&는 무조건 lvalue가 되어서 &&로 표현되는 리터럴과 임시값을 넣을 수 없다. 예를 들어서 예제의 LvalueFunction에는 500, int(120648395)같은 값을 전달할 수 없다. 그럼 좌측값, 우측값 매개변수 구분을 위해 const& T, T&&를 모두 오버로딩해야만 할까? 사실 그렇지 않다. 가령 예제의 RvalueFunction 함수는 rvalue만 받을 수 있을 것 같지만, auto&&는 모든 한정자에 대해 사용할 수 있다.

<C++ 예제 보기>
#!syntax cpp
import <utility>;

template<typename T>
T&& Function5_forwarding_by_template(T&& value) noexcept(noexcept(std::declval<T&&>()))
// 원본 자료형을 유지한채 value의 객체가 생성될 때 예외가 없음을 확인한다
{
    // lvalue, xvalue, prvalue 모두가 원래 값 범주(Value Category)를 유지한채, 아무 비용없이 전달된다
    // lvalue는 lvalue 그대로 전달된다
    // xvalue를 감싸 이름없이 전달한다
    // prvalue를 감싸 이름없이 전달한다
    return std::forward<T>(value);
}

template<typename T, typename V>
auto&& Function5_modified_forwarding_by_template(V&& value) noexcept(noexcept(std::forward_like<const volatile T>(std::declval<V&&>())))
// 원본 자료형을 바꾼 value의 객체가 생성될 때 예외가 없음을 확인한다
{
    // 원래 값 범주를 유지한채로, 다른 자료형으로 바꿔 전달할 수 있다.
    return std::forward_like<const volatile T>(value);
}

// C++23부터 사용할 수 있는 Function5_forwarding_by_template과 같은 코드
auto&& Function5_forwarding_by_deduction(auto&& value) noexcept(noexcept(std::declval<decltype(value)>()))
// 원본 자료형 그대로 value의 객체가 생성될 때 예외가 없음을 확인한다
{
    // C++23부터 가능한 완벽한 전달 수단
    return auto{ value };
    // 또는 
    return auto(value);
}

Position Function6_forwarding_by_copy(const Position& pos) noexcept(std::is_nothrow_copy_constructible<Position>)
{
    // pos를 복사해서 전달한다
    return pos;
}

Position&& Function6_forwarding_by_move(Position&& pos) noexcept(std::is_nothrow_move_constructible<Position>)
{
    // pos를 아무 성능 오버헤드 없이 그대로 전달한다
    return std::move(pos);

    // 이동 연산에 써도 문제 없다. 그러나 lvalue가 아님을 유의해야 한다
    return std::forward<Position>(pos);

    // 경고! 이 경우 복사가 되어 참조 Dangling이 일어난다
    return pos;
}
매개변수로 실체화하기 전, 인자에서는 &, &&를 멀쩡하게 갖고 있다. 그리고 이때 &가 여러번 중첩될 경우 & 또는 && 중 한 가지 경우로 압축한다. &&&앞에 있으면 &&가 되어버린다. 다시 말해서 static_cast<T&&>(T&)T&&로 연역된다. static_cast<const T&&>(T&)const T&&로 연역된다. 이 특성은 특이하게도 매개변수의 원본 자료형을 그대로 보존하는 효과가 나온다. 덕분에 auto에서 원본 자료형이 뭔지 알기 위해 decltype(auto)을 쓸 필요가 없다. 그리고 const auto&, auto&&를 모두 오버로딩 할 필요가 없다. 매개변수가 뭔지, 복사해야 할지 참조해야 할지 이동시켜야 할지 고민할 필요를 없애준다.

====# 사용 예제 #====
#!syntax cpp
// 템플릿, constexpr, 조건부 noexcept 사용
template<typename T>
constexpr T Instantiate(T&& obj, const float x, const float y, const float z) noexcept(std::is_nothrow_constructible_v<T, std::add_rvalue_reference_t<T>>)
{
    // 완벽한 전달(Perfect Forwarding) 사용
    return T(std::forward<T>(obj), x, y, z);
}

// 가변 템플릿, constexpr, 조건부 noexcept, 후속 반환형 사용
template<typename... Ts>
constexpr auto ForwardAsTuple(Ts&&... args) noexcept(std::conjunction_v<std::is_nothrow_constructible<Ts, Ts&&>...>)
    -> std::tuple<Ts&&...>
{
    // 완벽한 전달(Perfect Forwarding) 사용
    return std::tuple<Ts&&...>{ std::forward<Ts>(args)... };
}

int main()
{
    const long A = 0; // 0
    unsigned long long B = 93140583732; // 1
    bool C = false; // 2
    bool& D = C; // 3
    Squirrel E{}; // 4
    const Squirrel& F = E; // 5
    constexpr unsigned G = 34275860428; // 6

    auto tuple1 = std::make_tuple(A, B, C, D, E, F, G);
    std::get<2>(tuple1) = true; // 복사본은 원본 C, D에 영향을 주지 못함

    auto tuple2 = ForwardAsTuple(A, std::move(B), C, D, E, F, Squirrel{}, std::move(G));
    std::get<2>(tuple2) = true; // C, D가 true가 됨
    std::get<4>(tuple2).myName = "new name"; // E, F의 myName이 "new name"이 됨

    return 0;
}
여기서 tuple1은 복사본 튜플이 되어 std::tuple<long, unsigned long long, bool, bool, Squirrel, Squirrel, unsigned>로 생성된다. 그러나 tuple2는 완벽한 전달을 수행하여 std::tuple<const long&, unsigned long long&&, bool&, bool&, Squirrel&, const Squirrel&, Squirrel&&, const unsigned&&>가 된다. 참고로 이 기능을 수행하는 함수는 표준 라이브러리에 이미 std::forward_as_tuple, std::tie가 있으므로 굳이 또 구현할 필요는 없다.

10.3. 클래스

10.3.1. 템플릿 데이터 멤버

10.3.2. 템플릿 멤버 함수

10.3.2.1. Deducing this

10.3.3. 템플릿 자료형 멤버

#!syntax cpp
template<typename T>
class Trait
{
public:
    using type = T;
    using value_type = T;
    using const_type = const T;
    using reference = T&;
    using const_reference = const T&;
    using rvalue_reference = T&&;
    using const_rvalue_reference = const T&&;
    using pointer = T*;
    using const_pointer = const T*;
    using difference_type = std::ptrdiff_t;
};

template<typename _Ty, typename _Trait = Trait<_Ty>>
class MyVector
{
public:
    using value_type = _Trait<_Ty>::value_type;
    using const_type = _Trait<_Ty>::const_type;
    using reference = _Trait<_Ty>::reference;
    using const_reference = _Trait<_Ty>::const_reference;
    using rvalue_reference = _Trait<_Ty>::rvalue_reference;
    using const_rvalue_reference = _Trait<_Ty>::const_rvalue_reference;
    using pointer = _Trait<_Ty>::pointer;
    using const_pointer = _Trait<_Ty>::const_pointer;
    using difference_type = _Trait<_Ty>::difference_type;

    constexpr void Push(const_reference element);
    constexpr void Push(rvalue_reference element);

private:
    value_type* myBuffer;
};
자료형 별칭(Type Alias)

10.4. 제약조건

<C++ 예제 보기>
#!syntax cpp
import <print>;
import <type_traits>;

template<typename T>
concept integrals = std::is_integral_v<T>;

template<typename T>
void Print(T&& value)
{
    std::println("정수가 아닌 값 {} 출력", std::forward<T>(value));
}

// 후방에 선언되어 있어도 정수인 경우를 잘 걸러낼 수 있다.
template<integrals T>
void Print(T&& value)
{
    std::println("정수 값 {} 출력", std::forward<T>(value));
}

int main()
{
    // (1)
    // C++11의 메타 함수를 사용한 static_assert
    static_assert(std::is_integral_v<bool>);
    static_assert(std::is_integral_v<int>);
    static_assert(std::is_integral_v<unsigned int>);
    static_assert(std::is_integral_v<long long>);
    static_assert(std::is_integral_v<float>); // 컴파일 오류!
    static_assert(std::is_integral_v<double>); // 컴파일 오류!

    // (2)
    // C++20의 제약 조건을 사용한 static_assert
    static_assert(integrals<bool>);
    static_assert(integrals<char>);
    static_assert(integrals<int>);
    static_assert(integrals<unsigned int>);
    static_assert(integrals<long long>);
    static_assert(integrals<float>); // 컴파일 오류!
    static_assert(integrals<double>); // 컴파일 오류!

    // (3)
    // 템플릿에서 제약 조건 사용

    // (3-1)
    // "정수 값 10000 출력"
    Print(10'000);

    // (3-2)
    // "정수 값 10000000 출력"
    Print(10'000'000ULL);

    // (3-3)
    // "정수가 아닌 값 Hello, world! 출력"
    Print("Hello, world!");

    // (3-4)
    // "정수가 아닌 값 nullptr 출력"
    Print(nullptr);
}
Constraints (제약조건)
파일:상세 내용 아이콘.svg   자세한 내용은 C++/표준 라이브러리/concepts 문서
번 문단을
부분을
참고하십시오.

11. requires

Requires Clause (requires 절)

12. 특성 (Attribute)

파일:상세 내용 아이콘.svg   자세한 내용은 C++/문법/특성 문서
번 문단을
부분을
참고하십시오.


[호출] 연산자[2] 다중 스레드 환경에서 인자를 참조로 받으면 참조 대상 소실(Dangling)의 위험이 있어서 값으로 받는 경우가 있다[3] 이를 인라이닝(Inlining)이라고 한다[4] 예를 들어 extern 함수 또는 모듈에서 export된 함수[헤더] [6] 여기서 기본 생성자는 default가 아니여도 된다[헤더] [8] extern "C" { ...; }와 같이 스코프를 지정할 수 있다[9] 연산자 오버로딩은 가능하다[10] 전역 이름공간도 포함[11] 현재는 Zig 정도가 상수 표현식 기능을 제공한다