C++/문법

덤프버전 :

파일:나무위키+넘겨주기.png   관련 문서: C언어/문법

파일:나무위키+상위문서.png   상위 문서: C++



1. 개요
2. 자료형 (Data Types)
3. 함수
3.1. 정의
3.2. 구성
3.2.1. 평가 지시자
3.2.2. 반환 자료형
3.2.3. noexcept
3.2.4. 매개변수
3.2.4.1. 값으로 전달
3.2.4.3. 우측값 참조로 전달
4. 클래스 (Class)
5. 템플릿 (Template)
5.1. 변수
5.2. 함수
5.2.1. 템플릿 인자 추론
5.2.2. 완벽한 매개변수 전달
5.2.3. 사용 예제
5.3. 클래스
5.3.1. 템플릿 필드
5.3.2. 템플릿 메서드
5.3.3. Deducing this
6. 이름공간 (Namespace)
6.1. 용법
6.2. using namespace
6.3. Global Namespace
6.4. inline
6.4.1. Unnamed Namespace
6.5. 인자 의존성 탐색
7. 언어 연결성 (Language Linkage)
7.1. 내부 연결
7.2. 외부 연결
7.3. 모듈 연결
8. 저장소 지속요건 (Storage Duration)
8.1. static
8.2. extern
8.3. thread_local
8.4. mutable
9. 특성 (Attribute)



1. 개요[편집]


C++의 문법을 간략하게 설명하는 문서이다. C언어하고도 중첩되는 요소들이 많으므로 이 문서를 쉽게 이해하기 위해서는 C언어/문법 문서와 비교하여 참조하는 것이 좋다. 하지만 C 관련 문법 + 객체지향 문법만 안다고 해서 C++를 잘 아는건 아니다. 일단 C++11 이후로 추가된 기능이 엄청나게 많기 때문. 템플릿 공부도 많이 해야 한다. 여기에 쓰여있는것들은 핵심만 설명하며 응용을 가르쳐주진 않는다.
###include <iostream>

##int main()
##{
##    std::cout << "Hello, world!\n";
##    return 0;
##}
##


2. 자료형 (Data Types)[편집]


파일:나무위키상세내용.png   자세한 내용은 C++/문법/자료형 문서를 참고하십시오.



3. 함수[편집]



3.1. 정의[편집]


가장 많이 쓰는 함수의 개형

템플릿 함수의 개형

C++14에서 추가된 후속 반환형을 사용한 함수의 개형

문맥 지시자, 특성, 예외 사양, 템플릿까지 모두 사용한 가장 일반적인 함수의 개형

3.2. 구성[편집]



3.2.1. 평가 지시자[편집]


자료형 앞에
static
,
inline
,
constexpr
[C++11],
consteval
[C++20] 지시자를 지정할 수 있다.

  1. static
해당 함수가 정적 함수임을 나타낸다. 정적 함수는 정적 변수처럼 현재 코드 범위(Scope)에서 항상 같은 이름으로 같은 메모리 위치에 존재한다. 적어도 같은 범위에서는 같은 이름이 같은 함수임을 알 수 있다. 그러므로 C의 소스 구조에서 소스 파일이 아니라 헤더 파일에서 함수와 변수를 정의하기 위해 사용해왔다. 여러 곳에서 중복 삽입될 수 있는 헤더에서는 중복 객체 링킹 때문에 오직 선언만을 사용해야했다. 아니면 구조체 안에 필드 또는 메서드로 정의해야 했다.
static
함수가 헤더에 정의되어 있을 때 여러 곳에서 헤더를 삽입해도 오류없이 사용할 수 있다. 그러나 C++에서는 이름공간을 사용할 수 있기 때문에 의미가 퇴색된 바 있다. 그리고 C++20에서 모듈이 소개되었는데 아예
static
함수는 모듈 밖으로 내보낼 수 없다!
export
지시자를 사용해야 모듈 밖으로 객체를 내보낼 수 있는데
static
함수는 어찌된 일인지 내부 연결이 존재한다면서 컴파일 오류가 발생한다.
  1. inline
해당 함수의 쓰임새 부분이 함수의 코드로 대체될 수 있음을 나타낸다 [1]. 이 키워드가 적절하게 쓰이면 함수 호출 오버헤드를 줄이고, 호출 스택도 아낄 수 있어서 좋다. 심지어 최적화 과정에서 아예 인라인 함수의 코드를 날리고 결과값만 남길 수도 있다. 그렇지만 대부분의 현대 컴파일러는 알아서 인라인 처리를 해주므로 이 지시자의 의의는 더 적극적으로 인라인을 하라는 지시에 가깝다.
  1. constexpr
해당 함수가 상수 표현식임을 나타낸다. C++11에서 추가되었다. 상수 표현식, 또는 상수식은 실제 코드가 실행되는 런타임 시점이 아니라 바이너리가 생성되는 컴파일 시점에 실행 결과가 결정될 수 있는 식이다. 아무런 코드 연산의 과정이 컴파일 이후에는 남지 않기 때문이다. 즉 프로그램 이용자의 실행 시점에는 코드 실행 시간이 0이 되도록 최적화된다.
C++20에 와선 동적 메모리 할당 조차 컴파일 시점에 실행될 수 있다. 컴파일러는 이러면 할당 위치만 힙이고 작동은 스택처럼 구현되어 힙의 내용이 컴파일 시점에 결정된다. C++20에서 동적 배열 클래스
std::vector
, 문자열 클래스
std::string
constexpr
생성자, 소멸자, 메서드를 얻었다. 상수 표현식의 내용이 포괄적으로 바뀐 셈인데, 좋을 것만 같지만 이러다 보니 컴파일 시간이 너무 오래 걸리고, 키워드의 의미가 배보다 배꼽이 더 커지는 일이 일어났다. C++11에서는 진짜 O(1)인 함수였는데 C++17에서는 가능하다면 컴파일 시점에 평가해야 하는 함수가 되었고, C++23에 와서는 컴파일 시점에 평가될 수도 있는 함수가 되었다. 때문에 매크로도 대체할 겸 진짜 의도를 되살리기 위해 후술할
consteval
키워드가 도입되었다.
  1. consteval
해당 함수가 상수 표현식이고, 문맥 상으로도 반드시 컴파일 시점에 평가 되어야함을 나타낸다. 보면 알겠지만 맨 처음 도입된
constexpr
보다 더 엄격한 조건을 갖고 있다. 함수 내부와 매개 변수에서도 정적이고 컴파일 시점에 결정되는 자료형 또는 객체가 아니면 참조할 수 없다. 컴파일 시점에 값이 결정된다는 것은, 사용자 입장에서는 언제나 고정된 값으로 보인다는 뜻이다. 때문에
consteval
함수는 즉발 함수(Immediate Function)라고 불리며 어떤 부작용(Side Effect) 없이 독립적으로 실행되는 함수다[2]. 그래서
consteval
함수는 코드 상에서만 함수로 보이는, 사실상 상수라고 봐야 한다.
이 지시자의 의의는 컴파일러 전용 함수를 C++에 구현한다는 것에 있다.
constexpr
상수[3]는 C의 전처리기 키워드를 대체할 수 있었다.
consteval
은 이제 위험한 매크로의 일부를 대체할 수 있다. 예를 들어서 전처리기 분기를 통해[4] 어떤 상수를 선언한다면, 예전에는 전처리기와 매크로 지옥에서 빠져나오지 못했다. 이런 전처리기 구문들은 프로그램 헤더 구조의 저 멀리 최상단에 놓이게 될텐데 때문에 중복되는 헤더 삽입, 전처리기 키워드 중복 문제가 발생한다. 그러나 이젠
consteval
에서 C++ 코드를 통해 밖으로 보이지 않고 처리가 가능하다.

3.2.2. 반환 자료형[편집]


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


3.2.3. noexcept[편집]


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


3.2.4. 매개변수[편집]



3.2.4.1. 값으로 전달[편집]

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

    return 0;
}
Pass by value
함수의 매개변수로 사용하기 위해 인자를 값으로 호출하는것. 여기서 중요한것은, 값으로 전달하면 해당 인자는 함수의 범위 내에서 원본이 사용되지 않고, 함수의 매개변수로써 복사되어서 사용된다. 이를 정확한 용어로는 부패 (Decay)라고 한다. 예제의 함수들은 모두 인자
a
b
를 매개변수
x
에 복사하면서 원래 한정자를 잃어버린다. 함수 안에서는 매개변수인
x
를 증가시키기 때문에 원래 변수
a
,
b
에는 아무런 영향을 주지 못한다.


3.2.4.2. 좌측값 참조로 전달[편집]

파일:나무위키상세내용.png   자세한 내용은 참조에 의한 호출 문서를 참고하십시오.

import <iostream>;

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

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

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::cout << a; // (3)을 제외하면 2를 출력함

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

    // (5)
    const double& value_d = 3.141592368; // 어떤 실체가 없는 리터럴 값이지만 const&에 저장할 수 있음

    return 0;
}
Pass by lvalue reference
매개변수에 좌측값 참조자를 사용하면 인자로 전달된 변수를 그대로 가져온다. 참조형 매개변수는
lvalue
이며 일반적인 참조형 변수처럼 원본이 존재하는, 이름만 존재하는 변수다. 그래서
&
또는
std::addressof
로 원본 변수의 주소를 얻을 수 있다.

상단의 부패를 계속 설명하자면 여기서도 유의할 점이 있다. 불필요한 복사를 막으려면 인자를 참조형으로 받아야 한다. 원래
auto
를 쓰면
&
,
&&
, C++ 배열
[]
이 모두 증발한 자료형이 추론된다. 후술하겠지만
auto
는 사실 템플릿과 똑같은 원리로 연역되기 때문이다. 그래서
void Function(auto value)
에서 매개변수
value
는 값에 의한 전달을 수행한다. 여기서
const&
한정자를 사용하면 모든 종류의 값을 받을 수 있다. 가령
500UL
따위의
prvalue
, 또는 임시 객체 등의
xvalue
도 받을 수 있으므로
const&
를 쓰면 문제가 거의 발생하지 않는다. 그러므로 값을 수정하지 않는 인터페이스는 매개변수에
const&
를 사용하는 것이 좋다.


3.2.4.3. 우측값 참조로 전달[편집]

void Function1(int&& value);

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

constexpr Position Function2_copy(const Position& pos) noexcept
{
    return Position{ pos };
}

constexpr Position Function2_move(Position&& pos) noexcept
{
    return Position{ std::move(pos) };
    // 또는 수동으로 rvalue 변환
    return Position{ static_cast<Position&&>(pos) };
}

class Squirrel
{
public:
    std::string myName;
    Position myPosition;
};

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

constexpr void Function3_move(Squirrel& squirrel, Position&& pos) noexcept
{
    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);
Pass by rvalue reference
C++11에서 새로 도입된 인자 전달 방식이다.
&&
는 메모리에만 존재하거나, 임시 객체를 의미하거나, 혹은 저장되기 전에는 메모리에도 존재하지 않는 리터럴을 표현하기 위한 한정자다.
&&
는 이름이 없는 값이며, 변수가 사용되는 메모리 공간에 복사없이, 마치 처음부터 존재하는 것처럼 처리된다. 가령 복사가 아예 일어나지 않기에 값의 교환을 효율적으로 할 수 있다. 그러나 자료형 문서에서 설명했듯이
&&
는 불안정한 값이기 때문에 반드시
std::move
,
static_cast
따위로 감싸줘야 한다. 불안정하다는 것은 식별자의 존재때문에 일어나는 현상이다.
rvalue
는 이름이 없어야 하는데, 매개변수에 저장하는 순간 이름이 생기는 것이라서 식별자를 언급하는 순간
lvalue
가 되버린다. 만약 감싸지 않으면 함수 안에서는
&
,
const&
로 취급된다. 이러면 주소를 얻을 수 있고 값을 바꿀 수는 있겠지만 성능상의 이득은 사라진다.

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


4. 클래스 (Class)[편집]




}protected:const std::string myGreet = "Hello, world!";};
">
파일:나무위키상세내용.png   자세한 내용은 C++/문법/클래스 문서를 참고하십시오.



5. 템플릿 (Template)[편집]


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


5.1. 변수[편집]




5.2. 함수[편집]


// 매개 변수가 있고 반환값은 없는 함수
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 적용
}



5.2.1. 템플릿 인자 추론[편집]


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
는 바로 곧 템플릿이며, 각각이 다른 자료형으로 추론되는 템플릿일 뿐이다. 그렇기에
auto
나 템플릿이나 원래의 한정자가 부패하는 것이다. 이렇게 해야 함수 내부의 값과 외부의 값을 분리하고 의도치 않은 동작을 막을 수 있다. C++에서 의도하지 않은 동작은 모두 일어나서는 안되는 일이다. 사용자가 직접
&
,
const&
,
&&
*
따위의 한정자를 지정하지 않으면 컴파일러는 무조건 값에 의한 전달을 수행한다. 이를 막으려면 자료형 문서에서 설명한 것 처럼
T&&
또는
auto&&
로 완벽한 자료형을 얻어야 한다. 이를 함수에서 사용하는 방법은 다음 단락에서 설명한다.


5.2.2. 완벽한 매개변수 전달[편집]


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&
,
&&
를 모두 오버로딩해야만 할까? 사실 그렇지 않다. 가령 예제의
RvalueFunction
함수는
rvalue
만 받을 수 있을 것 같지만,
auto&&
는 모든 한정자에 대해 사용할 수 있다.
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<T&&>()))
// 원본 자료형 그대로 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&&
로 연역된다.
static_cast(T&)
const T&&
로 연역된다. 이 특성은 특이하게도 매개변수의 원본 자료형을 그대로 보존하는 효과가 나온다. 덕분에
auto
에서 원본 자료형이 뭔지 알기 위해
decltype(auto)
을 쓸 필요가 없다. 그리고
const auto&
,
auto&&
를 모두 오버로딩 할 필요가 없다. 매개변수가 뭔지, 복사해야 할지 참조해야 할지 이동시켜야 할지 고민할 필요를 없애준다.


5.2.3. 사용 예제[편집]








">
여기서
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&, const unsigned&&>
가 된다.

5.3. 클래스[편집]



5.3.1. 템플릿 필드[편집]



5.3.2. 템플릿 메서드[편집]



5.3.3. Deducing this[편집]




6. 이름공간 (Namespace)[편집]


namespace /*식별자*/
{
    ...;
}
이름 공간(Namespace)
이름공간, 또는 네임스페이스는 식별자 사이의 이름 충돌을 막기 위한 장치이다. 이름공간은 각각 분리된 프로그램처럼 존재하며 다른 이름공간끼리는 별도의 지시자없이는 참조할 수 없다 [5]. 당연히 지시자 없이는 다른 이름공간의 식별자를 참조할 수 없다. 프로젝트가 일정 규모 이상 커지거나, 외부 라이브러리를 사용하는 경우 이름이 같은 함수, 상수, 클래스[6] 등이 발생할 가능성이 커진다.

예를 들어 비슷한 용도의 반복적인 기저 코드 작성(Boilerplate)이 프로젝트에서 반복되다 보면 단어란 단어는 다 소모하고 비슷한 유틸리티 함수가 늘어난다. 이때 다른 라이브러리 사이에 같은 이름의 객체가 있으면 컴파일러는 이를 구분할 수 없다. 차라리 모호하다고 컴파일 오류를 내거나 런타임 오류라도 나면 다행이지만, 그렇지 않고 정상작동하는 것처럼 보이는 코드가 되면 예측할 수 없는 동작을 할 것이다. C언어에서 가장 큰 문제 중 하나가 프로젝트가 커질수록 식별자의 명칭이 겹칠 위험이 커지는 것이였다. C++에서 도입된 이름 공간은 큰 규모의 프로그램 개발에서 객체의 명명 문제를 대부분 해결해준다 [7].


6.1. 용법[편집]


import <print>;
import <string>;

namespace Namu
{
    class MyClass
    {
    public:
        void MyPrint() const noexcept
        {
            std::println(myCaption);
        }

    private:
        std::string myCaption = "나무위키";
    };
}

namespace Wiki
{
    class MyClass
    {
    public:
        void MyPrint() const noexcept
        {
            std::println(myCaption);
        }

    private:
        std::string myCaption = "NamuWiki";
    };
}

int main()
{
    Namu::MyClass class1;
    Wiki::MyClass class2;

    class1.MyPrint();
    class2.MyPrint();

    return 0;
}
이 소스 코드의 콘솔에서의 실행 결과는 다음과 같다.
나무위키

Namu Wiki

첫번째 줄에는 '나무위키'가 출력되고 두번째 줄에는 'NamuWiki'가 출력된다.


6.2. using namespace[편집]



using
지시자 (Using Directive)

using
을 이용하면 네임스페이스의 축약된 표현을 사용할 수 있다.

using [네임스페이스 이름]::[네임스페이스 멤버 이름];
[8]이라고 적은 경우를 생각해보자.
using [네임스페이스 x]::[클래스 y];
가 선언된 경우에는 클래스 y에 대한 인스턴스를 선언할 때에
x::y [인스턴스 이름];
이라고 적지 않고
y [인스턴스 이름]
이라고 적을 수 있다.
using [네임스페이스 x]::[멤버 함수 a];
가 선언된 경우에는 해당 함수를 호출할 때에
x::a(...);
이 아니라
a(...);
으로 호출할 수 있다.

using namespace [네임스페이스 이름]
을 적을 경우에는 클래스에 대한 인스턴스를 선언할 때에
[네임스페이스 이름]::[클래스 이름] [인스턴스 이름];
이라고 적지 않고
[클래스 이름] [인스턴스 이름];
이라고 적어도 된다. 이 경우에는 네임스페이스를 명시하지 않고도 해당 네임스페이스 안에 있는 모든 멤버를 호출할 수 있다.
using namespace [네임스페이스 x]
가 선언된 경우에는 이미
using [네임스페이스 x]::[클래스 y];
using [네임스페이스 x]::[멤버 함수 a];
를 선언한 것과 같아서
x::y [인스턴스 이름];
대신에 바로
y [인스턴스 이름]
이라고 인스턴스를 생성할 수 있고,
x::a(...);
이 아니라
a(...);
로 클래스 멤버 함수를 바로 호출할 수 있다.

가령 C++에서 가장 많이 선언되는
using namespace std;
의 경우에는 C++의 표준 라이브러리에 대한 네임스페이스인
std
를 명시하지 않고 생략하고 항상
std
안의 멤버를 사용하겠다는 뜻이다. 예를 들어
std::cout
대신에
cout
라고 축약해서 명시할 수 있다.

항목 5의 예제에서
#include
선언부 다음에
using namespace
namu;
를 적을 경우에는
main
함수에서
namu
네임스페이스에 있는
MyClass
클래스에 대한
class1
인스턴스를 선언할 때에
namu::MyClass class1;
라고 적지 않고
MyClass class1;
라고 축약해서 적을 수 있다.

using
은 헤더 파일에서 전역 범위로 사용하지 않는 것이 좋다. 이는 C++의
#include
문이 헤더를 그대로 복사 & 붙여넣기를 하는 식으로 작동하기 때문이다.
using
문을 사용하는 헤더를 포함하면 이를 포함한 모든 파일에
using
문이 강제적으로 적용되어 버리고,
using
을 취소할 방법이 있는 것도 아니기에 남의 코드를 직접 수정해야 하는 영 좋지 않은 상황이 만들어진다.


6.3. Global Namespace[편집]


import <print>;
import <string>;

class Namu
{
public:
    void print() const noexcept
    {
        std::println(myCaption);
    }

private:
    std::string myCaption = "Namu";
};

class Wiki
{
public:
    void print() const noexcept
    {
        std::println(myCaption);
    }

private:
    std::string myCaption = "Wiki";
};

namespace Full
{
    using ::Wiki; // ::로 사용 가능함

    using ::Namu;
    // 또는
    using Namu;
    // 또는
    using Namu = ::Namu;
}

int main(void)
{
    Namu class1;
    ::Wiki class2;

    class1.print();
    class2.print();

    return 0;
}
전역적 이름공간 (Global Namespace)
이름공간이 명시 되어있지 않을 때는 전역적 이름공간을 사용하게 된다. C언어도 기본적으로 전역적 이름공간을 사용한다. 이름 공간의 이름을 적지 않고
::
만을 사용하면 전역 이름공간을 사용하겠다고 지시할 수 있다.

그러나 전역 네임스페이스의 사용은 일반적으로 권장되지 않는다. 이름공간의 목적은 다른 역할을 하지만 중복되는 이름을 구분하기 위해 만들어 진 것인데 전역 네임스페이스는 그것을 무력화시키며 헤더에 전역 네임스페이스를 사용하게 되면 그 헤더를 삽입하는 소스 코드도 영향을 받기 때문이다. 설령 모듈이라도 이름의 중복은 피할 수 없는 문제다. 그렇기에 스코프 안에서만 사용하는 것이 좋다.


6.4. inline[편집]


// 중접 이름공간 선언
namespace NamuWiki::Documents::ProgrammingLanguage::inline FromEnhaWiki
{
    class Assemybly;
    class C;
    class Erlang;
    class Cpp;
    class Java
    class Python;
    class JavaScript;
    class Csharp;
    class Go;
    class Swift;
    class Rust;
}

namespace House::LoungeRoom::Furnitures
{
    inline namespace Y2019
    {
        class Television
        {
        public:
            std::string hwVender = "Samsung";
        };
        class Fan
        {
        public:
            std::string hwVender = "Dyson";
        };
        class Couch;
        class Chair;
        class LargeTable;
    }

    inline namespace Y2021
    {
        class Television
        {
        public:
            std::string hwVender = "Samsung";
        };
        class Fan // Y2019::Fan 클래스를 덮어쓴다.
        {
        public:
            std::string hwVender = "LG";
        };
        class AirConditioner;
        class MiniBar;
    }

    inline namespace Y2023
    {
        class Television // Y2021::Television 클래스를 덮어쓴다.
        {
        public:
            std::string hwVender = "LG";
        };
    }
}
인라인 이름공간 (Inline Namespace)
인라인 이름공간을 사용하면 선언한 이름공간의 내용물을 상위 이름공간에서도 바로 사용할 수 있다. 사실 인라인 이름공간의 원리는 상위 이름공간에 내용을 먼저 집어넣고, 인라인 이름공간 안에서
using
을 쓰는 것에 가깝다. 인라인 이름공간을 쓰면 이전에 선언된 이름공간의 식별자를 덮어쓴다. 이를 이용해 언어 내부에서 버전 관리를 할 수 있다. DirectX 혹은 WinUI 3에서 이런 식으로 기능을 업그레이드하고 있다.


6.4.1. Unnamed Namespace[편집]


namespace CPUVenders
{
    class Intel;
    class AMD;
    class [[deprecated]] VIA;
}

namespace
{
    class Nvidia;
    // CPUVenders::AMD, CPUVenders::Intel과 ::AMD, ::Intel은 다른 존재다.
    // 같은 객체로 취급하려면 using CPUVenders::...를 사용해야만 한다.
    class AMD;
    class Intel;
}

namespace HardwareVenders
{
    namespace
    {
        GigaByte,
        Asus,
    }
    // using (익명)::GigaByte;
    // using (익명)::Asus;

    namespace CPUVenders = ::CPUVenders;

    namespace GPUVenders
    {
        using ::Nvidia;
        using ::AMD;
        using ::Intel;
    }
}
이름 붙여지지 않은 이름공간 (Unnamed Namespace)
익명 이름공간은 이름공간을 선언할 때 식별자를 지정하지 않으면 된다. 이 경우 익명 이름공간이 선언된 스코프에서
using namespace (익명 이름공간);
을 사용하는 것과 같은 동작을 수행한다.


6.5. 인자 의존성 탐색[편집]


인자 의존성 탐색 (Argument Dependency Lookup)


7. 언어 연결성 (Language Linkage)[편집]


Language Linkage


7.1. 내부 연결[편집]


내부 연결 (Internal Linkage)
다른 프로그래밍 언어가 그렇듯, C++에는 스코프(Scope)라는 개념이 있다. 그 동안 프로그래밍에서 스코프라는 것이 정확히 뭔지 모르고 사용했을 것이다. C++에서는 이 조차 정확한 정의를 내리고 시작한다. C++의 스코프는 컴파일 과정에서 나타나는 코드 번역의 단위다. 쉽게 말해 식별자와 스택이 공유되는 문맥(Context)이다. 예를 들어 이름공간, 클래스의 메서드, 함수 등이 있다. 내부 연결은 문맥 안에서 선언된 식별자는 문맥 밖에서는 사용할 수 없는 상태를 말한다. 내부 연결은 밖에선 보이지 않기 때문에 당연히 이름의 중복, 모호성 문제가 발생하지 않는다. 가령 함수의 지역 변수와 매개변수, 전역 정적 변수와 함수, 그리고 모듈에서 내보내지 않은 식별자 등이 있다.


7.2. 외부 연결[편집]


외부 연결 (External Linkage)
외부에서 보이는 상태, 즉 인터페이스를 말한다. 현재 스코프 뿐만 아니라 다른 문맥에서 접근할 수 있다. 심지어 언어나 프로그램에서도 접근할 수 있다. 외부 연결이 지정된 객체는 선언만 있어도 된다. 이름 붙여지지 않은 이름공간(Unnamed Namespace)이 아닌 식별자가 있는 이름공간 안에 있는 객체들에는 외부 연결이 적용된다.


7.3. 모듈 연결[편집]


모듈 연결 (Module Linkage)
C++20의 모듈은
private
여부, 분할 모듈(Partition Module)인지 여부에 따라 다양한 규격이 지원된다. 이 모든 규격은 파일로 분리할 수 있으나 모듈 자체적으로는 이 파일들을 동일한 내부 상태, 파일 문맥, 번역 단위, 스코프로 취급하게 되어있다. 그러니까 모듈 내부의 파일이라면 아예 다른 파일이라도 헤더 삽입하는 것과 다를 바 없는 처리를 해준다. C++에서 이 파일들은 이제 같은 파일로 치고 컴파일을 해주기로 했는데 클래스나 변수를 못 쓰면 그것대로 이상한 일이므로 새로운 연결 상태가 도입된 것이다. 적용 대상은 내부 연결이 아니고,
export
하지 않은 객체에 적용된다 [9]. 만일 내부 연결이라면 사용할 수도 없을 것이다. 곧 모듈 연결은 내부 연결과 외부 연결의 중간 상태라고 말할 수 있다.


8. 저장소 지속요건 (Storage Duration)[편집]


Storage Duration
저장소 지속요건은 운영체제 메모리 모델과 깊은 연관이 있다. C++의 객체가 어떤 메모리에, 어느 위치에 할당되어서, 얼마나 오래 살아남는지를 정의한다.


8.1. static[편집]


class Device
{};

// 이름없는 이름공간 안에 있으면 무조건 내부 연결이 된다.
// 설령 extern 이라도.
namespace
{
    namespace InternalLinkage
    {
        class MyTablet : public ::Device
        {
        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;
}

// 구현 내용이 없으면 링크 오류가 발생한다.
// 그러나 내부 연결 객체는 현재 이름공간에 유일한 존재로 남으므로 식별자의 중복 선언 문제가 발생하지 않는다.
static std::string_view GetMyDeviceVender(std::string_view simple_namev) noexcept
{
    if (simple_name == "Desktop")
    {
        return MyDesktop::hwVender;
    }
    else if (simple_name == "Phone")
    {
        return InternalLinkage::MyPhone::hwVender;
    }
    else if (simple_name == "Tablet")
    {
        return InternalLinkage::MyTablet::hwVender;
    }
    else
    {
        return "No Device";
    }
}

// 이름 공간과 클래스 내의 구현 내용은 스코프 밖에, 그리고 헤더라면 별도의 소스 파일에 작성해야 한다. 
std::string InternalLinkage::MyTablet::hwVender = "Apple";
std::string InternalLinkage::MyTablet::hwIdentifier = "IPad 6th Gen";

std::string MyDesktop::hwVender = "TG삼보";
std::string MyDesktop::hwVender = "TG-DT281-GA51B-011";
정적인 내부 연결
C++에서 내부 연결을 표현할 때는
static
또는 이름없는 이름공간(Unnamed Namespace)을 사용할 수 있다.
static
은 변수와 함수에, 이름없는 이름공간은 C++의 모든 객체에 적용할 수 있다. C언어에서는
static
에는 내부 연결 외에도 문자 그대로 정적이라는 단서가 달려있다. 정적이라는 말은 (가상) 메모리 위치가 (프로그램 내에서는) 변하지 않는다는 뜻이다. 그렇기에 프로그램 안에서는 문맥 상관없이 모두가 참조할 수 있는 객체가 된다. 예를 들어서 클래스 내의 정적 필드와 메서드는 클래스 인스턴스를 만들 필요도 없이 클래스 이름만으로 사용할 수 있다. 이 특징은 내부 연결과는 상관없이 정적인 객체라서 생기는 현상이다. C++에는 메타클래스가 없으므로 클래스 자체는 메모리에 정적으로 고정된 객체다. 여기서 클래스의 멤버는 상대적 메모리 위치만 저장해서 구분한다. C++의 클래스는 정적 멤버가 아니더라도
MyPhone::callNumber
와 같이 접근할 수는 있다. 그러나 컴파일 오류가 발생하거나 메모리 접근 위반 0x00000016 참조! 따위의 런타임 오류가 발생할 것이다. 여기서
0x00000016
이 필드
callNumber
의 클래스
MyPhone
에 대한 상대적 주소다. 참고로 편의상 클래스의 정적 필드, 정적 메서드는 외부 연결로도 취급된다. 이 규칙이 없으면 헤더와 C++20 모듈에서 정적 필드와 메서드를 사용할 수 없을 것이다.

내부 연결을 가진 객체는 반드시 구현 내용을 갖고 있어야만 한다. 즉 인라인(inline)이여야만 한다. 만약 내부 연결 객체를 만들 때 값을 전달하지 않으면 어떻게 될까? 변수라면 기본값이 할당 되지만 함수는 선언만 할 수 없다. 내부 연결을 가진 클래스 인스턴스에서 별도로 생성자를 호출하지 않으면 인자가 없는 기본 생성자를 호출한다 [10]. 이때 생성한 인스턴스의 클래스에 기본 생성자가 없으면 내부 연결을 가질 수 없다.

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


8.2. extern[편집]


정적인 외부 연결
extern
은 두가지 용법이 있다.
static
과 상반되는 용도로 쓰이는 그냥
extern
, 그리고 C와 C++ 언어 모드를 전환할 때 쓰는
extern "C"
,
extern "C++"
이 있다. 당연히 C++에는 기본적으로
extern "C++"
이 적용된다. 모든 이름공간, 클래스와 변수 앞에 보이지 않는
extern "C++"
이 붙어있다고 생각하면 된다.
extern "C"
를 사용하면 C언어의 규칙을 따로 적용할 수 있다.
extern "C" { ...; }
와 같이 스코프를 지정할 수 있다.


8.3. thread_local[편집]


import <cstdlib>;
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();
    }

    return 0;
}
스레드 연결
변수 선언에 사용할 수 있다. 참고로 내부나 외부 연결에 관여하지는 않고, 상단 키워드들과 조합해서 쓸 수 있다. 전역 변수로 선언하면 이 순간부터 프로그램에서 사용한 모든 스레드에도 해당 변수가 선언되게 된다.


8.4. mutable[편집]


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. 특성 (Attribute)[편집]


파일:나무위키상세내용.png   자세한 내용은 C++/문법/특성 문서를 참고하십시오.



파일:크리에이티브 커먼즈 라이선스__CC.png 이 문서의 내용 중 전체 또는 일부는 2023-10-21 19:45:17에 나무위키 C++/문법 문서에서 가져왔습니다.

[C++11] [C++20] [1] 이를 인라이닝(Inlining)이라고 한다[2] 함수형 언어에서 함수와 정확히 같다[3] constexpr 변수는 무조건 상수이며, 컴파일 시점에 평가된다[4] 운영체제 플랫폼 구분 등[5] 이를 코드 범위(Scope)가 다르다고 한다[6] 이를 통틀어 객체라고 칭한다[7] 많은 객체 지향 언어가 그렇듯 C++에서도 이름이 같은 두 클래스가 속한 이름공간이 다르면 공존할 수 있다[8] 네임스페이스 멤버로는 클래스, 클래스 멤버 함수가 있다.[9] 참고로 모듈에서 내보내진 객체들은 외부 연결로 취급된다.[10] 여기서 기본 생성자는
default
가 아니여도 된다