※ C++11/14에서 새롭게 추가된  자동 타입 변환 키워드이다.

 

※ auto 사용 시 주의사항

  • auto 키워드는 함수 매개변수로 사용불가!
  • auto 키워드는 구조체나 클래스의 멤버 변수로 사용 불가( 객체 자료형 크기를 모르기 때문)
  • 가독성이 떨어짐으로 적절한 사용이 필요.

 

 

※ auto 키워드

선언된 변수의 초기화 식을 사용하는 것으로 컴파일러가 알아서 해당 타입을 추론 (type inference)하게 한다.

이 기능은 생성 시 변수를 초기화 할 때만! 작동한다. ( 초기화된 값을 기준으로 자료형을 선택해서)

※ auto를 이용한 자료형

int r = 20;
double PI = 3.14159265358979;

auto *pi = Π  // 포인터도 가능
auto &Pi = PI;  // 참조자도 가능

auto S = r*r*PI;  // double S
cout << S;  // 출력값: 1256.64

 

※ auto를 이용한 for문 출력

auto arr = { 1, 2, 3, 4 };
for (auto num : arr)
    cout << num << ' ';  // 1 2 3 4 출력
for (const auto&  p : photos) {
    wcout << "Photo location: " << (static_pointer_cast<Photo>(p))->location_ << endl;
}

 

※ auto를 이용한 함수

auto S(int r, double pi){
    return r*r*pi;
}

 

 

 

 

※ decltype 키워드

declared type 즉, 선언된 형식의 줄임말로 주어진 이름, 표현식의 구체적 타입을 알려주는 키워드이다.

이는 template에 기반한 제네릭 프로그래밍의 어려움을 해소하기 위해 도입되었다.

ex.

template<typename FirstType, typename SecondType>
??? AddArbitrary(FirstType first, SecondType second) {
    cout << first + second << endl;
}

int main() {
    AddArbitrary(1234, 'C');    // FirstType이 returnType
    AddArbitrary('C', 1234);    // SecondType이 returnType
}
template<typename FirstType, typename SecondType>
decltype(first+second) AddArbitrary(FirstType first, SecondType second) {  //Err!
    cout << first + second << endl;
}

 

§ auto vs decltype

- auto: 값에 상응하는 타입을 "컴파일러가" 추론하는 키워드

- decltype: 값으로부터 타입을 추출하는 키워드

template<typename FirstType, typename SecondType>
auto AddArbitrary(FirstType first, SecondType second) -> decltype(first+second){
    cout << first + second << endl;
}

auto +  ->decltype()을 이용해서 문제를 해결할 수 있다!

 

 

 

 

 

 

 

 

 

※ 범위기반 for문

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

vector<int> v;

int main() {
    int num;
    for (int i = 0; i < 5; i++){
        //cin >> v[i];  // 이건 왜 오류나는가
        cin >> num;
        v.push_back(num);
    }
    sort(v.begin(), v.end(), greater<int>());

    for (const auto& num : v)
        cout << num << " ";
}

C++11에서 새롭게 도입된 유형의 루프로 더 간단하고 안전하게 배열등의 모든 요소를 반복하는 방법이다.

§ 범위기반 for문의 가장 큰 특징

표현식 안의 모든 값에 대해 한번씩 루프를 실행!

범위기반 for문은 index 정보가 없고 배열요소를 변경할 수 없다.

즉, for문을 완전히 대체하지는 못한다.

따라서 컨테이너의 간단한 순회를 위해 사용된다!

보통 auto와 같이 사용되는 경우를 심심치 않게 볼 수 있기에 사용법을 알고 있는게 좋을 것이다.

 

※ 선언방법

for (자료형 변수 : 배열종류의 이름)

예를 들어 다음과 같이 선언할 수 있다.

순서는 다음과 같다.

int main() {
    int arr[] = { 1, 3 , 4, 5, 5, 8};
    for (int num :arr)
        cout << num << " ";
}

1. for문 실행, num에 arr의 첫 요소인 0이 할당

2. 그 후 0을 출력하는 cout 실행

3. 다시 for문 실행, 이 과정을 요소 8까지 반복

 

 

 

 

 

★ 배열에 대한 인덱스가 아닌, 배열의 요소값이 num에 할당된다!

여기서 자료형 변수 위치에 배열요소와 같은 자료형을 가져야 하기에 auto키워드로 자료형을 추론하게 하는 것이 좋다.

또한, 읽기 전용으로 사용하려할 때, (수정되지 않게끔) const 키워드를 붙여 다음과 같이 선언할 수 있다.

(성능상의 이유로 범위기반 for문에서 const 참조를 사용하는 것이 좋다.)

int main() {
    int arr[] = { 1, 3 , 4, 5, 5, 8};
    for (const auto& element :arr)
        cout << element << " ";
}

 

포인터로 변환된 배열 (array decay into pointer)의 경우, 배열의 길이를 몰라 범위기반 for문을 사용할 수 없다.

int sumArray(int array[]) {
    int sum = 0;
    for (const auto& number : array) // 컴파일 에러, 배열크기를 모름
        sum += number;

    return sum;
}

int main() {
    int array[5] = { 9, 7, 5, 3, 1 };
    cout << sumArray(array); // 배열이 포인터로 decay
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

※ 스마트 포인터 

// int *ptr=new int;    delete ptr;
std::unique_ptr<int> ptr = make_unique<int>();

//int *ptr=new int[5];
auto ptr=make_unique<int[]>(5);

//void (*ptr)()
std::function<void(void)>

C++ 프로그램에서 new 를 사용해 동적할당한 메모리는, 반드시 delete로  해제해야 한다.

 

C++에서는 메모리 누수(memory leak)로부터 프로그램의 안전성을 보장하기 위해 스마트 포인터를 제공 (memory 헤더).

 

스마트 포인터란 포인터처럼 동작하는 클래스 템플릿으로, 사용이 끝난 메모리를 자동으로 해제한다. (delete 자동 수행)

 

이는 new로 기본포인터(raw pointer)가 실제 메모리를 가리키도록 한 후

스마트 포인터 (smart pointer)에 기본포인터를 대입하여 사용한다.

이렇게 정의된 스마트 포인터의 수명이 다하면, 소멸자는 delete 키워드를 사용하여 할당된 메모리를 자동으로 해제한다.

 

 

※ 스마트 포인터의 종류

<C++11 이전>

auto_ptr (삭제됨)

 

<C++ 이후>

 

§ 참조 카운트 (reference count): 해당 메모리를 참조하는 포인터가 몇 개인지 나타내는 값

 

1. unique_ptr: 하나의 스마트포인터만이 객체를 가리킬 수 있도록 함

                        (reference cnt1을 넘길 수 없음)

2. shared_ptr: 하나의 특정 객체를 참조하는 스마트포인터의 개수를 참조

                        (reference cnt1씩 증가or감소, 참조횟수가 0이되면 delete되어 자동 해제)

3. weak_ptr: 하나 이상의 shared_ptr가 가리키는 객체를 참조 

                         (reference cnt늘리지 않음으로 shared_ptr 객체사이 순환참조를 제거하기 위함)

 

unique_ptr  (활용도: ★★★★★) 

사람이 실수할 수 있기에, unique와 move를 이용해 최대한 shared사용을 자제하는 것이 좋다.

unique_ptr 객체는 move() 멤버함수로 소유권을 이전할 수 있다. (단, 복사는 불가능!)

소유권이 이전되면 이전 unique_ptr 객체는 더이상 해당 객체를 소유하지 않게 재설정 된다.

 

#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main() {
    unique_ptr<int> ptr1(new int(10));
    //unique Pointer 초기화와 함께 int형 값 10으로 동적할당

    auto ptr2 = move(ptr1);
    //auto 키워드로 ptr2는 ptr1의 타입을 추론(unique_ptr<int> 타입)
    //move 키워드는 ptr1에서 ptr2로 메모리의 소유권을 이전하기 위해 사용된다.

    unique_ptr<int> ptr3 = ptr1; // Err
    //애초에 ptr1이 소멸되어 접근이 불가하다.
    //대입 연산자를 이용한 복사는 오류를 발생시킨다.

    if (ptr1 == nullptr) {
        cout << "I'm Dead. Call ptr2 instead." << endl;
        cout << ptr1 << endl;   // *ptr1이라 써주지 않아서 Err
    }
    cout << *ptr2 << endl;

    ptr2.reset();
    ptr1.reset();
    //reset 함수로 메모리 영역을 삭제할 수 있다.
}

 

 

<make_unique()함수를 이용>

- 전달받은 인수를 사용해 지정된 타입의 객체를 생성

- 생성된 객체를 가리키는 unique_ptr을 반환하여 예외발생에 대해 안전히 대처가능

#include <iostream>
#include <memory>
#include <string>

using namespace std;

class Animal {
public:
    string name = "";
    Animal() { cout << "construct" << endl; };
    Animal(string name) {
        this->name = name;
        cout << "construct" << endl;
    }
    ~Animal() { cout << "destruct" << endl; };

    void setName() {
        cout << name << " : Grrr!" << endl;
    }
};

int main() {
    // Animal 객체 생성
    unique_ptr<Animal> a = make_unique<Animal>("Tiger");
    a->setName();
}

//------------출력------------//
//construct
//Tiger : Grrr!
//destruct

 

 

shared_ptr

shared_ptr은 하나의 특정 객체를 참조하는 스마트 포인터가 총 몇 개인지 를 참조하는 스마트 포인터

reference count는 특정 객체에 새로운 shared_ptr이 추가될 때마다 1씩 증가하고, 수명이 다하면 1씩 감소하며

추가되었던 shared_ptr이 해제되어 참조 카운트가 0이 되면 delete 가 자동으로 진행, 메모리를 자동해제한다.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main() {
    shared_ptr<double> ptr1(new double(123.456));
    cout << ptr1.use_count() << endl;

    auto ptr2(ptr1);
    cout << ptr2.use_count() << endl;

    auto ptr3(ptr2);
    cout << ptr3.use_count() << endl;
}

//------------출력------------//
// 1
// 2
// 3

 

 

<make_shared()함수를 이용>

- shared_ptr의 객체를 안전하게 만들 수 있다.

- 전달받은 인수를 사용해 지정된 타입의 객체를 생성, 생성된 객체를 가리키는 shared_ptr을 반환.

- 이 함수도 예외 발생에 대해 안전하다.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Monster {
public:
    Monster() { cout << "construct" << endl; }
    ~Monster() { cout << "destruct" << endl; }
};

int main()
{
    shared_ptr<Monster> mst_ptr1 = make_shared<Monster>();
    cout << mst_ptr1.use_count() << endl;

    auto mst_ptr2 = mst_ptr1;
    cout << mst_ptr1.use_count() << endl;

    mst_ptr2.reset();
    cout << mst_ptr1.use_count() << endl;
}

//------------출력------------//
// construct
// 1
// 2
// 1
// destruct

 

 

 

 

weak_ptr

shared_ptr은 참조 카운트를 기반으로 동작하는 스마트 포인터

 

하나 이상의 shared_ptr 객체가 소유하는 객체에 대한 접근을 제공하지만, 참조 카운트에 포함되지 않는다.

 

만약에 서로가 상대를 가르키는 shared_ptr을 가지고 있다면, 참조 횟수는 절대 1 이하로 내려가지 않기 때문에,

0이 되어야 자동으로 해제되는 스마트 포인터에 가장 크리티컬한 문제가 된다.

이렇게 서로가 상대를 참조하는 상황을 순환 참조(Circular Reference)라고 한다.

 

weak_ptr은 바로 이러한 shared_ptr 인스턴스 사이의 순환 참조를 제거하기 위해서 사용한다.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Monster {
public:
    weak_ptr<Monster> otherMonster; 
    // shared_ptr로 선언할 경우 순환 참조 발생하기에 weak_ptr로 선언하여 순환 참조를 예방
    Monster() { cout << "생성" << endl; }
    ~Monster() { cout << "소멸" << endl; }
};

int main() {
    //철수와 민수에 대한 shared_ptr을 선언
    shared_ptr<Monster> chul_su = make_shared<Monster>();
    shared_ptr<Monster> min_su = make_shared<Monster>();

    // reference count : 참조 카운트
    cout << "철수 reference count : " << chul_su.use_count() << endl;
    cout << "철수 reference count : " << min_su.use_count() << endl;

    chul_su->otherMonster = min_su;
    min_su->otherMonster = chul_su;

    cout << "철수 reference count : " << chul_su.use_count() << endl;
    cout << "민수 reference count : " << min_su.use_count() << endl;
}

//------------출력------------//
// 생성
// 생성
// 철수 reference count : 1
// 철수 reference count : 1
// 철수 reference count : 1
// 민수 reference count : 1
// 소멸
// 소멸

 

 

 

 

 

 

 

※ 스마트 포인터 형변환(typecasting)

- static_pointer_cast

- dynamic_pointer_cast

- const_pointer_cast

 

포인터를 사용할 때, 다른 포인터 타입으로의 캐스팅은 용이했다.

하.지.만! shared_ptr 등의 스마트 포인터는 그렇지가 않다.

이를 위해서 ((AnotherClass*)(ptr.get())) 와 같이 강제로 포인터를 얻어 캐스팅을 해줄 수 있지만 전혀 C++ 답지 못하다.

 

따라서 static_pointer_cast  /  dynamic_pointer_cast  / const_pointer_cast 가 추가되었다.

이를 통해 안전하고도 편한 스마트 포인터 캐스팅이 가능해진 것이다.

vector<shared_ptr<MediaAsset>> assets;

assets.push_back(shared_ptr<Song>(new Song(L"Himesh Reshammiya", L"Tera Surroor")));
assets.push_back(shared_ptr<Song>(new Song(L"Penaz Masani", L"Tu Dil De De")));
assets.push_back(shared_ptr<Photo>(new Photo(L"2011-04-06", L"Redmond, WA", L"Soccer field at Microsoft.")));

vector<shared_ptr<MediaAsset>> photos;

copy_if(assets.begin(), assets.end(), back_inserter(photos), [] (shared_ptr<MediaAsset> p) -> bool {
    // Use dynamic_pointer_cast to test whether
    // element is a shared_ptr<Photo>.
    shared_ptr<Photo> temp = dynamic_pointer_cast<Photo>(p);
    return temp.get() != nullptr;
});

for (const auto&  p : photos) {
    // We know that the photos vector contains only
    // shared_ptr<Photo> objects, so use static_cast.
    wcout << "Photo location: " << (static_pointer_cast<Photo>(p))->location_ << endl;
}

 

 

 

 

코드출처) 

https://bbagwang.com/programming/cpp/%EC%8A%A4%EB%A7%88%ED%8A%B8-%ED%8F%AC%EC%9D%B8%ED%84%B0-smart-pointer/

 

https://ence2.github.io/2020/11/c-%EC%BA%90%EC%8A%A4%ED%8C%85-%EC%B4%9D%EC%A0%95%EB%A6%AC%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%8F%AC%EC%9D%B8%ED%84%B0-%EC%BA%90%EC%8A%A4%ED%8C%85-%ED%8F%AC%ED%95%A8/

※ 형식연역 (type deduction)

형식연역이 일어나는 방식을 확실히 이해하지 않는다면 Modern C++에서 효과적인 프로그래밍은 불가능하다.

1장에서는 이런 형식연역의 작동방식과 형식연역에 기초한 auto와 decltype의 작동방식에 대해 알아본다.

필요성 : 템플릿 형식 연역 규칙들이 auto의 문액에 적용 될 때 덜 직관적이며
auto를 잘 활용하려면 템플릿 형식 연역을 이해하고 있어야 한다.

 

 

 

 

 

User가 System의 작동방식을 알지 못해도 System의 일에 만족한다면, 잘 설계되었다라고도 할 수 있다.

 

그런 프로그래머라면 좋은 소식과 나쁜 소식이 있다.

- Positive: Modern C++은 아주 강한 기능 중 하나인 auto가 template 형식영역을 기반으로 작동한다는 것이다.

- Negative: template형식 연역규칙들이 auto의 문맥에 적용될 때 template에 비해 덜 직관적인 경우가 있다.

 

∴ 따라서 auto를 잘 활용하려면 auto가 기초하는 template 형식연역의 면모를 제대로 이해해야한다.

 

 

 

※ 함수 template

template<typename T>
void f(ParamType param);

// 어떤 표현식으로 f를 호출
f(expr);

컴파일 도중 expr을 이용해 2가지의 형식을 연역한다.

1. T에 대한 형식

2. ParamType에 대한 형식

 

이 두 형식이 다른 경우가 많은데 이는 ParamType에 흔히 const 같은 참조한정사 수식어가 붙기 때문이다.

template<typename T>
void f(const T& param); //ParamType은 const T&

int x = 0;

f(x); // int로 f를 호출

이 경우, T는 int로 연역되지만 ParamType은 cosnt int& 로 연역된다. (x는 int이고 T는 int로 연역된다.)

 

T에 대해 연역된 형식이 함수에 전달된 인수의 형식과 같다고 생각했다면 그것이 항상 그러지는 않는다.

=> T에 대해 연역된 형식은 expr의 형식에 의존할 뿐만 아니라 ParamType의 형태에도 의존한다.

그 형태에 따라 총 3가지 경우로 나뉜다.

1. ParamType이 포인터, 참조형식이지만 보편참조는 아닌 경우.
2. ParamType이 보편참조인 경우
3. ParamType이 포인터, 참조 모두 아닌 경우

따라서 3가지 형식 연역 시나리오에 대해 살펴봐야 한다.

 

다음과 같은 일반적인 형태의 템플릿과 그 호출에 기초해 3가지 시나리오를 생각해보자.

template<typename T>
void f(ParamType param);

f(expr);  // expr로부터 T와 ParamType을 연역

 

 

Case 1. ParamType이 포인터, 참조형식 이지만 보편참조는 아님.

1. 만약 expr이 참조형식이면 참조부분을 무시한다.

2. 그 후 expr의 형식을 ParamType에 대해 pattern-matching방식으로 대응시켜 T의 형식을 결정한다.

template<typename T>
void f(T& param);  // param은 참조형식

int x = 27;        // x는 int
const int cx = x;  // cx는 const int
const int& rx = x; // rx는 const int인 x에 대한 참조

/* 여러 호출에서 param과 T에 대해 연역된 형식 */
f(x);  // T는 int, param의 형식은 int&
f(cx); // T는 const int, param의 형식은 const int&
f(rx); // T는 cosnt int, param의 형식은 const int&

위 f(cx), f(rx)의 param은 const값이고, 이는 그 객체가 수정되지 않을 것이라 기대한다.

즉, 해당 매개변수가 const에 대한 참조일 것이라 기대한다. (T& 매개변수를 받는 템플릿에 const객체를 전달해도 안전한 이유)

 

f(rx)에서 비록 rx의 형식이 참조이지만 T는 비참조로 연역된 것에 주목하면 이는 형식연역과정에서 rx의 참조성이 무시되기 때문임을 알 수 있다.

만약 f의 매개변수 형식을 T&에서 const T&로 바꾸게 된다면 상황이 달라지지만 큰 변화는 없다.
cx와 rx의 const성은 유지되며 단, param이 const에 대한 참조로 간주되므로  
const가 T의 일부로 연역될 필요는 없다.
template<typename T>
void f(const T& param);  // param이 cosnt에 대한 참조

int x = 27;        // x는 int
const int cx = x;  // cx는 const int
const int& rx = x; // rx는 const int인 x에 대한 참조

/* 여러 호출에서 param과 T에 대해 연역된 형식 */
f(x);  // T는 int, param의 형식은 const int&
f(cx); // T는 int, param의 형식은 const int&
f(rx); // T는 int, param의 형식은 const int&

이전처럼 rx의 참조성은 형식연역과정에서 무시된다.

 

이는 아래처럼 param을 참조가 아닌 포인터, const를 가리키는 포인터라도 형식연역은 본직적으로 같은 방식을 취한다.

template<typename T>
void f(T* param);  // param이 cosnt에 대한 참조

int x = 27;        // x는 int
const int *px = x; // px는 const int로 x를 가리키는 포인터

/* 여러 호출에서 param과 T에 대해 연역된 형식 */
f(&x);  // T는 int, param의 형식은 int*
f(px); // T는 const int, param의 형식은 const int*

 

여기까지는 너무 쉬워서 하품이 날 지경이라고 한다.(물론 저자가 그런거지 난 아니다. 어려워 죽을거 같다.)

 

 

 

 

 

 

Case 2. ParamType이 보편참조임.

템플릿이 보편참조매개변수를 받는 경우, 상황이 불투명해진다.

그런 매개변수 선언은 rvalue 참조와 같은 모습이다. (보편참조의 선언형식은 T&&이다.)

l-value가 전달되면 r-value참조와는 다른 방식으로 행동한다.

 

- 만약 expr이 l-value면, T와 ParamType 둘 다 l-value 참조로 연역된다. (비정상적 상황)

  i) 템플릿 형식 연역게서 T가 참조형식으로 연역되는 경우, 이것이 유일

  ii) ParamType의 선언구문은 r-value 참조와 같지만 연역된 형식은 l-value참조이다.

- 만약 expr이 r-value면, '정상적인' 규칙들이 적용된다. (Case 1의 규칙들.)

template<typename T>
void f(T&& param);  // 이번에는 param이 보편 참조

int x = 27;         // x는 int
const int cx = x;   // cx는 const int
const int& rx = x;  // rx는 const int인 x에 대한 참조

f(x);  // x는 l-value, 따라서 T는 int&, param 형식도 int&
f(cx); // cx는 l-value, 따라서 T는 const int&, param 형식도 const int&
f(rx); // rx는 l-value, T는 const int&, param 형식도 const int&
f(27); // 27은 r-value, 따라서 T는 int, param 형식은 int&&

나중에 item 24에서 설명될 것이고, 지금은 보편참조매개변수에 관한 형식연역 규칙들이

l-value참조나 r-value참조 매개변수들에 대한 규칙들과는 다르다는 점만 기억하면 된다.

특히 보편참조가 관여하는 경우, l-value와 r-value에 대해 서로 다른 연역규칙이 적용된다. (보편참조가 아니면 발생X)

 

 

 

 

 

 

 

Case 3. ParamType이 포인터도 아니고 참조도 아님

ParamType이 포인터도 참조도 아닌 경우, 인수가 함수에 값으로 전달되는 pass-by-value인 상황이다.

template<typename T>
void f(T param);     // param이 값으로 전달된다.

따라서 param 주어진 인수의 복사본 (즉, 완전히 새로운 객체)이다.

param이 새로운 객체라는 사실 때문에 expr에서 T가 연역되는 과정에서 다음과 같은 규칙들이 적용된다.

1. 이전처럼, 만약 expr의 형식이 참조라면, 참조부분은 무시된다.
2. expr의 참조성을 무시한 후 만약 expr이 const라 해도 그 const 역시 무시한다.

만약 volatile객체라도 그것도 무시한다.   (volatile객체는 흔하지 않기에 (장치구동기 구현시 사용) item 40 참고)

 

다음은 이 규칙들이 적용되는 예이다.

template<typename T>
void f(T&& param);  // 이번에는 param이 보편 참조

int x = 27;         // x는 int
const int cx = x;   // cx는 const int
const int& rx = x;  // rx는 const int인 x에 대한 참조

f(x);  // T와 param의 형식 둘 다 int
f(cx); // 여전히 T와 param의 형식 둘 다 int
f(rx); // 여전히 T와 param의 형식 둘 다 int

이때, cx와 rx는 const값이지만, param은 const가 아님을 주목하자.

param은 cx, rx의 복사본이므로 (param은 cx, rx와 달리 완전히 독립적인 객체이므로) 당연한 결과이다.

cx, rx가 수정될 수 없다는 점(const)은 param의 수정가능 여부와는 무관하기 때문이다.

param의 형식을 연역하는 과정에서 expr의 const성이 무시되는 이유가 바로 이것이다.

expr을 수정할 수 없다해서 그 복사본까지 수정할 수 없는 것은 아니기 때문이다.

 

여기서 명심할 점은, const가 값 전달 매개변수에 대해서만 무시된다는 점이다.

즉, const에 대한 참조나 포인터 매개변수의 경우, 형식연역과정에서 expr의 const성이 보존된다.

 

 

그러나 expr이 cosnt객체를 가리키는 const포인터이고 param에 값으로 전달되는 경우는 어떨까?

template<typename T>
void f(T param);                    // 인수는 param에 여전히 값으로 전달됨

const char* const ptr = "V2LLAIN";  // ptr은 const 객체를 가리키는 const 포인터

f(ptr);                             // const char* const 형식의 인수를 전달

포인터 선언 오른쪽 const로 ptr 자체가 const가 된다. (const char* const ptr)

즉, ptr을 다른 장소를 가리키도록 변경불가능! (nullptr도 배정할 수 없다.)

 

포인터 선언 왼쪽 const가 가리키는 것은 문자열이 const인 것을 의미한다. (const char* const ptr)

 

ptr을 f에 전달하면 그 포인터를 구성하는 bit들이 param에 복사되며 포인터자체(ptr)는 값으로 전달된다.

형식연역과정에서 ptr의 const성은 무시되며 따라서 param에 연역되는 형식은 const char*이다.

즉, 형식연역과정에서 ptr이 가리키는 것의 const성은 보존되나

ptr 자체의 const성은 ptr을 복사해 새 포인터 param을 생성하는 도중 사라진다!

 

 

 

※ 템플릿 연역과정에서 배열이나 함수이름에 해당하는 인수는 포인터로 붕괴(decay)한다.

(단, 그런 인수가 참조를 초기화하는데 사용되면, 그 경우에는 포인터로 붕괴하지 않는다.)

 

§ 배열과 포인터를 구분하지 않고 사용가능하지만, 배열형식은 포인터 형식과 다르다는 사실!

- 배열과 포인터를 맞바꿔 쓸 수 있는 것처럼 보이는 환상의 주된 원인은 많은 문맥에서 배열이 배열의 첫 원소를 가리키는 포인터로 붕괴한다(decay)는 점이다.

이런 붕괴때문에 아래와 같은 코드는 오류가 없이 컴파일 된다.

const char name[] = "V2LLAIN";
const char* ptrname = name;

 

그런데 배열을 값 전달 매개변수로 받는 template에 전달하면 어떻게 될까?

우선 배열형식의 함수 매개변수라는 것은 없다!

물론 아래와 같은 구문 자체는 가능하다.

void myFunc(int param[]);

하지만 이 경우 배열선언은 하나의 포인터 선언으로 취급되므로 사실상 아래와 동일하게 취급된다.

void myFunc(int *param);  // 위와 동일한 함수

 

따라서 아래 예시를 보면 name은 배열이지만 T는 const char*로 연역되어 버린다.

const char name[] = "V2LLAIN";

template<typename T>
void f(T param);      
f(name);             // name은 배열이지만 T는 const char*로 연역됨

 

 

그런데 한가지 교묘한 요령이 있는데, 함수의 매개변수를 진짜 배열로 선언은 못하지만,

배열에 대한 참조로 선언할 수는 있다!

 

즉, 다음과 같이 템플릿 f가 인수를 참조로 받도록 수정하고 함수에 배열을 전달하면

T에 대해 연역된 형식은 배열의 실제형식이 된다!

const char name[] = "V2LLAIN";

template<typename T>
void f(T& param);      // 참조 전달 매개변수가 있는 템플릿
f(name);               // 배열을 f에 전달

 

배열에 대한 참조선언을 이용하면 배열에 담긴 원소들의 개수를 연역하는 템플릿을 만들 수 있다!

 

[item 15]에서 더 자세히 나오지만 이 함수를 constexpr로 선언하면 함수 호출의 결과를 컴파일 도중 사용할 수 있다!

그렇게 되면 기존 배열과 같은 크기의 새 배열을 선언하는 것이 가능하게 된다.

template<typename T, std::size_t N>

constexpr std::size_t arraySize(T (&) [N]) noexcept { return N; }

int Vals[] = {1, 2, 3, 4 ,5, 6, 7};  // Vals의 원소개수는 7
int mapVals[arraySize(Vals)];  // mapVals의 원소 개수 역시 7개

/*물론, modern C++ 개발자라면 std::array나 vector를 더 선호할 것이다.*/
std::array<int, arraySize(Vals)> mapVals;  // mapVals의 크기는 7

이때, arraySizenoexcept 선언한 것은 컴파일러가 더 나은 코드를 산출하는데 도움을 주기 위한 것이다. (item 14)

 

 

물론, C++에서 포인터로 붕괴하는 것이 배열만 있는 것은 아니다.

앞에서 배열에 대한 형식연역과 관련된 모든것은 함수포인터로의 붕괴에 적용된다.

void someFunc(int, double);

template<typename T>
void f1(T param);       // f1의 param은 값 전달 방식

template<typename T>
void f2(T& param);      // f2의 param은 참조 전달 방식

f1(someFunc);           // param은 함수포인터로 연역됨 // void (*)(int, double)
f2(someFunc);           // param은 함수참조로 연역됨   // void (&)(int ,double)

 

 

 

 

 

 

◈ Remind ◈

- 템플릿형식 연역 중 참조형식의 인수들은 비참조로 취급된다. 즉, 참조성이 무시된다.
- 보편참조 매개변수에 대한 형식 연역과정에서 l-value들은 특별하게 취급된다.
- 값 전달방식의 매개변수에 대한 형식연역과정에서 const, volatile 인수는 비 const, 비 volatile 인수로 취급된다.
- 템플릿형식 연역과정에서 배열이나 함수이름에 해당하는 인수는 포인터로 붕괴한다.
  단, 그런 인수가 참조를 초기화하는데 쓰이는 경우, 포인터로 붕괴하지 않는다.

+ Recent posts