mir.pe (일반/밝은 화면)
최근 수정 시각 : 2024-03-01 21:58:17

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. 언어 연결성 (Language Linkage)
6.1. 내부 연결6.2. 외부 연결6.3. 모듈 연결
7. 저장소 지속요건 (Storage Duration)
7.1. static
7.1.1. static 변수7.1.2. static 함수7.1.3. 클래스 멤버
7.2. extern7.3. thread_local7.4. mutable
8. 상수 표현식 (Constant expression)9. 템플릿 (Template)
9.1. 변수9.2. 함수
9.2.1. 템플릿 인자 추론9.2.2. 완벽한 매개변수 전달
9.3. 클래스
9.3.1. 템플릿 데이터 멤버9.3.2. 템플릿 멤버 함수
9.3.2.1. Deducing this
9.3.3. 템플릿 자료형 멤버
9.4. 제약조건
10. 이름 탐색 (Name Lookup)
10.1. 인자 의존성 탐색10.2. 모호한 함수 후보 문제 & 이름공간 무시10.3. 잘못된 함수 후보 문제10.4. 해결 방법
11. 특성 (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. 언어 연결성 (Language Linkage)

Language Linkage
다른 프로그래밍 언어가 그렇듯, C++에는 스코프(Scope)라는 개념이 있다. 그 동안 프로그래밍에서 스코프라는 것이 정확히 뭔지 모르고 사용했을 것이다. C++에서는 이 조차 정확한 정의를 내리고 시작한다. C++의 스코프는 컴파일 과정에서 나타나는 코드 번역의 단위다. 코드 번역의 단위는 식별자와 스택이 공유되는 문맥(Context) 혹은 프로그램일 수도 있다. 스코프 안에서 정의한 객체들은 스코프 내부 혹은 스코프를 참조하는 다른 스코프에서 사용할 수 있다. 작게는 함수 내부부터, C언어부터 내려온 헤더(Header)가 바로 하나의 번역 단위다. C++20에서는 모듈(Module)이라는 새로운 소스 단위도 도입됐다.

6.1. 내부 연결

<C++ 예제 보기>
#!syntax cpp
// 이름없는 이름공간 안에 있으면 무조건 내부 연결이 된다.
// 설령 extern 이라도.
namespace
{
    namespace InternalLinkage
    {
        class MyTablet
        {
        public:
            static std::string hwVender;
            static std::string hwIdentifier;
        };

        class MyPhone : public ::Device
        {
        public:
            // constinit 사용
            static inline constinit std::string hwVender = "Google";
            static inline constinit std::string hwIdentifier = "Pixel 5";

        protected:
            std::string callNumber;
            std::size_t callCounts;
            std::string IMEI;
        };
    }

    class MyDesktop : public Device
    {
    public:
        static std::string hwVender;
        static std::string hwIdentifier;
    };

    // extern이지만 내부 연결 함수
    extern std::string_view GetMyDeviceID(std::string_view simple_name) noexcept;
}

std::string InternalLinkage::MyTablet::hwIdentifier = "IPad 6th Gen";

std::string MyDesktop::hwVender = "TG삼보";
std::string MyDesktop::hwVender = "TG-DT281-GA51B-011";
내부 연결 (Internal Linkage)
내부 연결은 단 하나의 번역 단위 안에서만 사용할 수 있는 상태를 의미한다. 내부 연결은 외부와 독립된 상태이기 때문에 당연히 이름의 중복, 모호성 문제가 발생하지 않는다. 다시 말하면 현재 번역 단위(헤더 및 헤더를 삽입하는 소스 파일들) 안에서는 유일한 존재로 특정되는, 고유한 이름을 갖고 있다는 뜻이다.

내부 연결 객체들은 유일한 존재임이 보장되어야 하며 이는 공짜가 아니다. 변수의 경우 값이 대입된 경우와 그렇지 않은 경우가 혼재되어서는 안된다. 함수는 상기한 inline 규칙이 강제된다. 곧 매개변수 자료형과 noexceptC++17 등 서명이 같아야 비로소 같은 함수임을 보장받을 수 있다. 클래스/결합체(Union)의 경우 부모 클래스, 멤버, 특성(Attribute), 메모리 정렬이 같아야 한다. 열거형(Enumeration)의 경우 부모 자료형, 멤버 열거자, 특성, 메모리 정렬이 같아야 한다.

또한 유일한 존재라는 말은 해당 객체의 구현이 다른 어디에도 없다는 것인데, 곧 객체의 구현을 반드시 선언과 같이 해줘야 한다. 변수는 값을 할당해주거나 기본값으로 초기화가 가능해야 하고, 함수는 구현을 즉시 해줘야 하며, 클래스와 열거형 역시 전방위 선언만 있으면 안되고 구현 몸체(Class Body)가 있어야 한다.

C++에선 코드 번역 단위(문맥)가 전환될 때 자동으로 내부 연결이 적용된다. 가령 삽입하지 않은 헤더의 객체는 사용할 수 없다. 그리고 함수의 매개변수와 함수 안에서 선언한 지역 변수는 함수 밖에서 사용하지 못한다. 소스 파일에만 선언한 객체는 헤더에서 사용할 수 없다. 또한 클래스의 비정적 멤버도 일종의 내부 연결로 취급되어서, 클래스의 중복 정의는 일어나도 클래스 메서드의 중복 정의는 일어나지 않는다.

사용자가 직접 내부 연결을 표현할 때는 static 연결성 지시자 또는 이름없는 이름공간(Unnamed Namespace)C++17 을 사용할 수 있다 [6].

6.2. 외부 연결

외부 연결 (External Linkage)
외부 연결은 외부에서 보이는 상태, 즉 인터페이스를 말한다. 외부 연결이 지정된 객체는 구현이 없이 선언만 있어도 된다. 왜냐하면 객체가 현재 번역 단위 뿐만 아니라 다른 곳에 존재할 수 있기 때문이다. 이렇게 외부로 노출된다는 건 외부 의존성이 생길 수 있고 다른 문맥에서 사용할 수 있음을 의미한다. 더 나아가 다른 문맥에서 접근할 수 있다는 말은 다른 언어나 프로그램에서도 사용할 수 있다는 뜻이다. 이 기능으로 매우 강력한 응용이 가능하다. 대표적인 예시로 Apple macOSDyLib이나 Microsoft WindowsDLL이 있다. 외부 라이브러리를 쓰는 소스를 보면 외부 연결로 지정된 클래스와 함수는 있으나 구현부는 없는데, 함수의 구현부를 운영체제의 도움을 받아 외부에서 가져와서 사용한다.

C++에서 헤더의 삽입 그 자체가 외부 연결이다. 헤더의 삽입은 헤더 파일의 복사 붙여넣기와 똑같은 동작이다. 링커에선 이것을 외부 연결로 해석한다. 그래서 소스 파일에서 헤더를 삽입하고 헤더에서 선언한 함수를 구현하는 기작이 가능한 것이다 [7]. 사실상 C언어에서 작성했던 모든 객체는 외부 연결이라고 할 수 있다. C++에서도 거의 변하진 않았으나 클래스의 publicstatic 멤버(자료형 별칭, 필드, 메서드)도 외부 연결로 취급된다. protected는 부분적으로 외부 연결인걸로 취급한다. 생뚱맞은 규칙 같지만 이 규칙이 없으면 클래스의 정적 멤버를 아예 사용할 수 없을 것이다.

사용자가 명시적으로 외부 연결을 표현할 때는 이름공간, extern 연결성 지시자 사용 또는, 모듈에서 exportC++20 를 하면 된다.

6.3. 모듈 연결

모듈 연결 (Module Linkage)C++20
C++20의 모듈은 이전까지 사용되던 헤더를 대체할 수 있는 소스 파일 규격이다. 모듈은 export 지시자로 선택적으로 객체를 모듈 밖으로 내보낼 수 있다. 모듈의 구현은 분할 모듈(Partition Module), private 구현 등 다양한 규격이 지원된다. 분할 모듈은 파일로 분리할 수 있다. 그리고 헤더와 .cpp파일처럼 별도의 구현부를 만들 수도 있다 [8]. 헤더처럼 모든 객체를 복사 붙여넣기하는 게 아니라, 사용자가 선택적으로 인터페이스를 만들 수 있다는 장점이 있다. 한편 모듈을 링크하려면 export한 객체에는 외부 연결이, export하지 않은 객체는 아예 없는 존재로 취급되던가 혹은 내부 연결이 적용되어야 하는데... 이러면 문제가 발생한다. 이유는 바로 내부 연결의 제약 때문이다.

객체를 선택적으로 내보낸다는 것은 클래스의 특징을 따라 은닉성을 파일 범위로 확장한 거라고 볼 수 있다. 그러나 클래스의 protected, private과는 달리 내부 연결은 외부에서는 사용할 수도 없는 데다가 객체(함수, 클래스)의 정의가 선언과 동반되어야 하는 등 까다로운 규칙이 있다. 내부 연결의 특징 때문에 모듈 외부는 물론이고, 분할 모듈 및 private 구현부에서조차 사용하지 못할 것이다. export하지 않은 클래스나 함수라도 구현을 따로 만들 수는 있어야 하지 않겠는가? 산하의 모듈 파일들을 같은 번역 단위로 치고 컴파일 해주려는데, 기껏 만든 함수나 클래스를 못 쓰면 안되므로 새로운 연결 상태가 도입된 것이다. 곧 모듈 연결은 내부 연결과 외부 연결의 중간 상태라고 말할 수 있다. 대상은 모듈에서 내부 연결이 아니고, export 하지 않은 객체에 적용된다.

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

Storage Duration

7.1. static

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

7.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 변수가 클래스의 인스턴스이고, 초기값을 할당하지 않았다면 그 변수는 클래스의 기본 생성자를 호출한다 [9]. 때문에 기본 생성자가 없으면 오류가 발생할 수 있으니 주의해야 한다.

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

7.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 함수가 헤더에 정의되어 있을 때에는 여러 곳에서 헤더를 삽입해도 링크 오류없이 사용할 수 있다

7.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이 정확하게 이 목적에 부합한다.

7.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++" [11]과 같이 사용한다. 기본적으로 C++의 모든 객체에는 extern "C++"이 적용된다. 모든 이름공간, 클래스와 변수 앞에 보이지 않는 extern "C++"이 붙어있다고 생각하면 된다. 그리고 extern "C"를 사용하면 함수 오버로딩 금지 등 C언어의 규칙을 따로 적용할 수 있다 [12].

7.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)
이름공간[13], 클래스에서 변수 선언에 사용할 수 있다. 전역 변수 선언에 사용하면 이 순간부터 프로그램에서 사용한 모든 스레드에 해당 변수가 선언되게 된다. 참고로 내외부 연결성에 관여하지는 않으므로, static 이나 extern과 조합해서 쓸 수 있다.

7.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 메서드에서도 수정할 수 있다. 상단의 예제는 대표적으로 쓰이는 스핀락의 구현이다.

8. 상수 표현식 (Constant expression)

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

9. 템플릿 (Template)

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

9.1. 변수

9.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 적용
}

9.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&&로 완벽한 자료형을 얻어야 한다. 이를 함수에서 사용하는 방법은 다음 단락에서 설명한다.

9.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가 있으므로 굳이 또 구현할 필요는 없다.

9.3. 클래스

9.3.1. 템플릿 데이터 멤버

9.3.2. 템플릿 멤버 함수

9.3.2.1. Deducing this

9.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)

9.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 문서
번 문단을
부분을
참고하십시오.

10. 이름 탐색 (Name Lookup)

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

int main()
{
    std::complex left{ 30, 5 };
    std::complex right{ 60, 2 };

    // (1-1)
    // `*` 연산자는 `std` 이름공간안에 있는데 실행할 수 있다
    // result_1: std::complex<int> == std::complex{ 1700, 360 }
    auto result_1 = left * right;

    // (1-2)
    // std::cout은 이름공간 `std` 안의 클래스 `std::iostream<char, std::char_traits<char>>`의 extern 인스턴스다
    // std::endl은 이름공간 `std` 안의 endl(std::ostream&) 함수다
    // << 연산자는 이름공간 `std` 안에 있다
    // std::operator<<(std::ostream&, const std::complex&)을 실행한다
    std::cout << "result_1 = " << result_1 << std::endl;

    // (2)
    // `std::complex`에 대한 `cos`의 오버로딩은 C언어의 `cos`와 다르게 `std` 이름공간 안에 있는데 실행할 수 있다
    // result_2: std::complex<int> == std::complex{ 11, 73 }
    auto result_2 = cos(left);

    // (2-2)
    // << 연산자는 이름공간 `std` 안에 있다
    // std::operator<<(std::ostream&, const int&)을 실행한다
    std::cout << "imaginary number of result_2 = " << result_2.img() << std::endl;
}
위 코드는 표준 라이브러리를 써서 복소수를 계산하고 출력하는 예제다. 여기서 C++의 오묘한 특징이 나타난다. 우리는 연산자 오버로딩을 무의식적으로 사용하고 있었지만, 사실 아주 이상한 상황이다. 분명 연산자도 함수라서 이름공간에 제약을 받을텐데 대신 자연스럽게 컴파일되고 문제도 없다. 저들은 모두 이름공간 std 안에 있다. 그러나 using namespace std; 지시자 없이도 무사히 연산자 수행이 가능하다. using std::operator~;를 쓸 필요도 없다. 대체 C++ 컴파일러는 무슨 조화를 부린 것일까?

이때 컴파일러가 내세우는 규칙은 바로 이름 탐색 (Name Lookup)이다. 사실 이 규칙은 모든 객체에 적용된다. 연산자에만 특별 규칙을 둔 게 아니라 공평하게 적용되는 규칙이다. 어떤 식별자가 사용되면 컴파일러는 가능한 후보를 어디에 있든지 모두 찾아낸다. 사용자는 코드에 이름을 적었을 뿐이지만 컴파일러는 식별자가 유일하지 않을 가능성, 클래스, 함수, 변수, 이름공간 넷중에 헷갈릴 가능성, const 여부나 값 범주 문제 등등등 고려해야 할 점이 많다. 다시 말해서 모든 표현식에서 사용 가능한 클래스. 함수, 변수, 이름공간의 후보를 결정하는 규칙이 바로 이름 탐색이다. 이 규칙은 C언어 C++의 경계를 나누는 핵심 요소라고 말할 수 있다.

10.1. 인자 의존성 탐색

<C++ 예제 보기>
#!syntax cpp
namespace NamuWiki
{
    struct Vector2
    {
        // Vector2는 float 필드 두개 뿐이므로 복사와 이동이 가능하다

        // 이항 양의 부호 연산자 (덧셈 연산자)
        Vector2 operator+(Vector2& rhs) noexcept
        {
            Vector2 result = *this; // *복사
            result.x += rhs.x;
            result.y += rhs.y;

            return result; // *RVO
        }

        // 이항 음의 부호 연산자 (뺄셈 연산자)
        // 매개변수가 꼭 const lvalue일 필요는 없으나 성능 때문에 하는 게 좋다
        // 이때 `operator+`는 구조체 Vector2 내부가 아니라 이름공간 NamuWiki에 정의된다
        friend Vector2 operator-(const Vector2& lhs, const Vector2& rhs) noexcept
        {
            // C++의 결집 구조체 초기화 (Aggregate initialization)
            return Vector2{ lhs.x - rhs.x, lhs.y - rhs.y };
        }

        // 곱셈 연산자
        // 매개변수가 꼭 const lvalue일 필요는 없으나 성능 때문에 하는 게 좋다
        // 이때 `operator+`는 구조체 Vector2 내부에 정의된다
        // inline이 아니라서 정의를 늦게 해줘도 된다
        Vector2 operator*(const Vector2& lhs, const Vector2& rhs) noexcept;

        // 곱셈 연산자
        // 매개변수가 꼭 const lvalue일 필요는 없으나 성능 때문에 하는 게 좋다
        // 이때 `operator/`는 구조체 Vector2 내부에 정의된다
        // 그러나 inline이기 때문에 반드시 클래스 정의와 함께 같은 헤더 어딘가에 정의해줘야 한다
        inline Vector2 operator/(const Vector2& rhs) noexcept;

        // 단항 양의 부호 연산자
        // 이때 `operator+`는 구조체 Vector2 내부가 아니라 이름공간 NamuWiki에 정의된다
        static friend Vector2& operator+(Vector2& vector) noexcept
        {
            vector.x = std::abs(vector.x);
            vector.y = std::abs(vector.y);
            return vector;
        }

        // 단항 음의 부호 연산자
        // 단항 음의 부호 연산자는 friend가 아니면 static일 수 없다.
        // 이때 `operator-`는 구조체 Vector2 내부에 정의된다
        Vector2& operator-() noexcept
        {
            x = -std::abs(x);
            y = -std::abs(y);
            return *this;
        }

        // 단항 비트 반전 연산자
        // 단항 비트 반전 연산자는 friend가 아니면 static일 수 없다
        // 이때 `operator~`는 구조체 Vector2 내부가 아니라 이름공간 NamuWiki에 정의된다
        static friend Vector2& operator~(Vector2& vector) noexcept
        {
            vector.x = std::abs(vector.x);
            vector.y = std::abs(vector.y);
            return vector;
        }

        float x, y;
    };

    Vector2 Vector2::operator/(const Vector2& rhs) noexcept
    {
        // C의 지정 초기화 (Designated initialization)
         return Vector2
         {
             .x = this->x / rhs.x,
             .y = this->y / rhs.y
         };
    }

    Vector2 Add(const Vector2& lhs, const Vector2& rhs) noexcept
    {
        return lhs.operator+(rhs);
    }

    Vector2 Subtract(const Vector2& lhs, const Vector2& rhs) noexcept
    {
        return lhs.operator-(rhs);
    }

    struct Vector3 : public Vector2
    {
        float z;
    }

    Vector3 Add(const Vector3& lhs, const Vector2& rhs) noexcept
    {
        return Vector3{ lhs.x + rhs.x, lhs.y + rhs.y, lhs.z };
    }
}

NamuWiki::Vector2 NamuWiki::operator*(const NamuWiki::Vector2& lhs, NamuWiki::const Vector2& rhs) noexcept
{
    return Vector2{ lhs.x * rhs.x, lhs.y * rhs.y };
}

int main()
{
    NamuWiki::Vector2 vec1{ 50, 100 };
    NamuWiki::Vector2 vec2{ 400, 70 };
    NamuWiki::Vector3 vec3{ 200, 910, 30 };

    // (1)
    // NamuWiki::Vector2 내부의 (this const Vector2&, const Vector2&)를 이항 +연산자 호출
    vec1 += vec2;

    // (2)
    // result_2 == NamuWiki::Vector2{ 540, 170 }
    // NamuWiki 내부의 (const Vector2&, const Vector2&)를 받는 이항 +연산자 호출
    auto result_2 = vec1 + vec2;

    // 3)
    // result_3 == NamuWiki::Vector2{ 50, 100 }
    // NamuWiki 내부의 (const Vector2&, const Vector2&)를 받는 이항 -연산자 호출
    auto result_3 = vec1 -= vec2;

    // (4)
    // result_4 == NamuWiki::Vector2{ -400, -70 }
    // NamuWiki::Vector2 내부의 단항 -연산자 호출
    auto result_4 = vec2.operator-();

    // (5)
    // result_5 == NamuWiki::Vector2{ -2000, -7000 }
    // NamuWiki 내부의 (const Vector2&, const Vector2&)를 받는 이항 *연산자 호출
    auto result_5 = NamuWiki::operator*(vec1, vec2);

    // (6)
    // result_6 == NamuWiki::Vector2{ -350, -30 }
    // NamuWiki 내부의 (const Vector2&, const Vector2&)를 받는 함수 Add 호출
    auto result_6 = Add(vec1, vec2);

    // (7)
    // result_7 == NamuWiki::Vector3{ 250, 1010, 30 }
    // NamuWiki 내부의 (const Vector3&, const Vector2&)를 받는 함수 Add 호출
    auto result_7 = Add(vec1, vec3);
}
인자 의존성 탐색(Argument Dependency Lookup)
만약 이름 탐색이 함수의 이름을 찾는데 이용되면 인자 의존성 탐색(Argument Dependency Lookup)이라고 칭한다. 의존성 탐색이라는 것은 컴파일러가 실행할 수 있는 함수의 후보를 찾아내는 걸 의미한다. 후보에는 함수가 아닌 객체의 선언(함수와 이름이 같은 클래스나 변수도 마찬가지), 다른 클래스의 멤버를 올리진 않는다. C++에서 이 규칙 덕분에 함수와 연산자의 오버로딩을 할 수 있고, 클래스, 이름공간, 함수와 변수의 이름이 전부 같아도 구별할 수 있다. 가령 생성자만 해도 클래스와 이름이 같은데 멀쩡하게 존재할 수 있지 않은가?

이는 분명히 장점이 많으나 문제도 많다. 함수는 인자 추론, 의존성 탐색을 통해 의도하지 않은 함수를 실행할 수 있다. 이렇게 찾아낸 함수 후보가 원하는 게 아닐 가능성이 있다는 것이다. 이것 때문에 표준 라이브러리 변경사항에 Qualifying Namespace std, Preventing ADL 같은 게 가득해진 원인이 됐다. 특히 표준 라이브러리의 범용 알고리즘과 숫자 연산 함수들이 이런 문제가 매우 컸다. 그리고 여러 라이브러리에서 같은 자료형에 대해 같은 이름의 함수를 작성했다면 오동작을 일으킬 가능성이 크다. 다음 단락에서 말하겠지만 무려 세가지 치명적인 문제를 발생시킨다. 간단히 소개하자면 모호한 함수 후보 문제, 잘못된 함수 후보 문제, 이름공간 무시가 일어난다.

10.2. 모호한 함수 후보 문제 & 이름공간 무시

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

int main()
{
}
한정되지 않은 인자 의존성 탐색 (Unqualified Argument Dependency Lookup)

10.3. 잘못된 함수 후보 문제

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

int main()
{
}
한정된 인자 의존성 탐색 (Qualified Argument Dependency Lookup)
그러면 함수 사용하는 부분에 모조리 이름공간을 붙이면 해결이 될까? 안타깝게도 그렇지 않다.
사용자의 함수 오버로딩과 오버라이딩 무시가 발생한다.

10.4. 해결 방법

<C++ 예제 보기>
#!syntax cpp
namespace NamuWiki
{
    long long Add(int lhs, int rhs);

    class Calculator
    {
    public:
        long long operator()(long long value) const noexcept;
    };

    Calculator Calculator{};
}
long long Calculator(long long a);

int Add(int a, int b);

int main()
{
    // (1)
    auto result_1 = Calculator(254906214713);

    // (2)
    // NamuWiki::Calculator의 인스턴스 Calculator에서 () 연산자 호출
    auto result_2 = NamuWiki::Calculator(254906214713);

    // (3)
    // 전역 범위의 ::Add 호출
    auto result_3 = Add(1000, 6000);

    // (4)
    // 오류! 클래스 Calculator는 () 연산자로 실행할 수 없습니다
    // C++23 부터는 static operator()가 가능하다
    using namespace NamuWiki;
    auto result_4 = NamuWiki::Calculator(254906214713);

    // (5)
    // 오류! 모호한 함수 후보
    using namespace NamuWiki;
    auto result_5 = Add(1000, 6000);
}
함자 (Functor, Function Object)
함자 또는 함수 객체는 C++ 특유의 () 연산자의 오버로딩을 통해 함수처럼 실행할 수 있는 클래스다. 함자 클래스의 인스턴스를 만들고, 함수와 똑같이 인스턴스의 식별자에 ()와 인자를 전달하면 () 연산자가 수행된다.

<C++ 예제 보기>
#!syntax cpp
class Functor
{
public:
    int operator()() const noexcept
    {
        return 500;
    }

    double operator()(double add) const noexcept
    {
        return myDouble + add;
    }

    double myDouble;
};

class Adder
{
public:
    int operator()(const int& lhs, const int& rhs) const noexcept
    {
        return multiplier * (lhs + rhs);
    }

    float operator()(const float& lhs, const float& rhs) const noexcept
    {
        return static_cast<float>(multiplier) * (lhs + rhs);
    }

    int multiplier = 1;
};

int main()
{
    // (1) test1.myDouble == 0
    Functor test1;

    // test1_result: int == 500
    auto test1_result = test1();

    // (2) test2.myDouble == 200.0
    // ()로 객체 생성
    Functor test2( 200.0 );

    // test2_result: double == 1200.0
    auto test2_result = test2(1000);

    // (3) test3.multiplier == 1
    Adder test3{};

    // test3_result: int == 17000
    auto test3_result = test3(8000, 9000);

    // (4) test4.multiplier == 7
    Adder test4{ 7 };

    // test4_result: float == 4200.0f
    auto test4_result = test3(300, 300.0f);
}
앞서 말했듯이 이름 탐색 후보에 클래스의 멤버는 포함되지 않는다. 마찬가지로 클래스의 () 연산자는 인스턴스에 직접 접근하기 전까지는 의도하지 않은 ADL에서 안전하다. 클래스의 비정적 메서드는 내부 연결의 특징이 있어 클래스 안의 함수 정의가 외부에 보이지 않는다.

C++20에서는 알고리즘 함수의 문제를 해결하기 위해 모듈 <ranges>와 이름공간 std::ranges까지 추가하고, 함자에다가 concept[15]를 아득바득 발라서 해결을 봤다. 거기다가 이 함자들은 니블로이드(Niebloid)라는 특별한 존재다. 니블로이드는 어떤 이름공간 안에 있고, 인스턴스를 생성하려면 특별하고 숨겨진 방법 뿐이 없고, 복사, 이동, 대입이 불가능하고, 상속도 불가능하다. 자명한 클래스의 완전한 대척점의 특징을 가지고 있다.

11. 특성 (Attribute)

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


[호출] 연산자 [2] 다중 스레드 환경에서 인자를 참조로 받으면 참조 대상 소실(Dangling)의 위험이 있어서 값으로 받는 경우가 있다 [3] 이를 인라이닝(Inlining)이라고 한다 [4] 예를 들어 extern 함수 또는 모듈에서 export된 함수 [헤더] [6] static은 변수와 함수에, 이름없는 이름공간은 C++의 모든 객체에 적용할 수 있다 [7] 주의할 점은 헤더의 경우 선언만 넣지 않으면 중복된 객체 구현으로 인한 링크 오류가 발생할 가능성이 크다. 이는 변수와 함수에 흔히 발생하는 문제다 [8] 다시 말해서 동일한 헤더 삽입하는 것과 다를 바 없는 처리를 해준다는 뜻이다 [9] 여기서 기본 생성자는 default가 아니여도 된다 [헤더] [11] extern "C" { ...; }와 같이 스코프를 지정할 수 있다 [12] 연산자 오버로딩은 가능하다 [13] 전역 이름공간도 포함 [14] 현재는 Zig 정도가 상수 표현식 기능을 제공한다 [15] 템플릿 제약조건, 추후 설명