본문 바로가기
개인공부/Rookiss C++ 프로그래밍 입문

Chapter 12 - Modern C++

by 하고싶은건많은놈 2023. 3. 29.

algorithm

자료구조 : 데이터를 저장하는 구조
알고리즘 : 데이터를 어떻게 사용할 것인가?

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

int main()
{
    srand(static_cast<unsigned int>(time(nullptr)));
    vector<int> v;

    for (int i = 0; i < 100; i++)
    {
        int num = rand() % 100;
        v.push_back(num);
    }

    // find()
    {
        int number = 50;
        vector<int>::iterator itFind = find(v.begin(), v.end(), number);
        if (itFind == v.end())
            cout << "not found" << endl;
        else
            cout << "found" << endl;
    }

    // find_it()
    {
        struct CanDivideBy11
        {
            bool operator()(int n)
            {
                return (n % 11) == 0;
            }
        };
        vector<int>::iterator itFind = find_if(v.begin(), v.end(), CanDivideBy11());
        if (itFind == v.end())
            cout << "not found" << endl;
        else
            cout << "found" << endl;
    }

    // count_if()
    {
        struct isOdd
        {
            bool operator()(int n)
            {
                return (n % 2) != 0;
            }
        };
        int n = count_if(v.begin(), v.end(), isOdd());
        cout << n << endl;

    // all_of() / any_of() / none_of()

        bool b1 = all_of(v.begin(), v.end(), isOdd());
        bool b2 = any_of(v.begin(), v.end(), isOdd());
        bool b3 = none_of(v.begin(), v.end(), isOdd());
        cout << b1 << "\t" << b2 << "\t" << b3 << endl;
    }

    // for_each()
    {
        struct MultiplyBy3
        {
            void operator()(int& n)
            {
                n = n * 3;
            }
        };
        for_each(v.begin(), v.end(), MultiplyBy3());
    }

    // remove() / remove_if()
    {
        struct isOdd
        {
            bool operator()(int n)
            {
                return (n % 2) != 0;
            }
        };
        vector<int>::iterator it = remove_if(v.begin(), v.end(), isOdd());
        // 1 4 3 5 8 2 => 4 8 2 5 8 2

        v.erase(it, v.end());
        //v.erase(remove_if(v.begin(), v.end(), isOdd());
    }

    return 0;
}

auto

Modern C++ (C++11)부터 사용된 문법
형식 연역(type deduction)이라고도 불림
추론 규칙은 생각보다 복잡할 수 있음

int main()
{
    int& reference = a;
    const int cst = b;

    auto test1 = reference;
    auto test2 = cst;

    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);

    for (vector<int>::size_type i = 0; i < v.size(); i++)
    {
        int& data = v[i];
        //auto& data = v[i];
        data = 100;
    }

    return 0;
}

기본 autoconst&를 무시함

가독성이 저하되기 때문에 auto만 사용하는 것은 권장하지 않음
단, 타이핑이 길어지는 경우에는 사용함


중괄호 초기화 {}

int main()
{
    int a = 0;
    int b(0);
    int c{0};

    vector<int> v1;
    v1.push_back(1);
    v1.push_back(2);
    v1.push_back(3);
    vector<int> v2(10, 1);
    vector<int> v3{1, 2, 3, 4};

    return 0;
}

중괄호 초기화는 vector등의 컨테이너와 잘 어울림
축소 변환을 방지할 수 있음(축소 변환시 에러 발생)
함수 선언과 변수 초기화를 명확히 구분할 수 있음

여러개를 초기화할시 initializer_list<> 생성자를 사용함
이는 다른 생성자보다 우선권이 높기 때문에 문제가 생길 수 있음


nullptr

기존에는 0과 NULL을 사용

void Test(int a)
{
    ...
}

void Test(void* ptr)
{
    ...
}

int main()
{
    Test(0);
    Test(NULL);
    Test(nullptr);
}

함수가 오버로딩 되어있을 경우 0이나 NULL을 사용하면 포인터를 활용할 때 문제가 생길 수 있음
즉, 위와 같은 경우 Test(0)Test(NULL)은 모두 int형을 매개변수로 하는 버전으로 작동함
반면 nullptr은 명확히 포인터 타입을 가리키기 때문에 Test(nullptr)void* 형을 매개변수로 하는 버전으로 작동함
또한 가독성 측면에서도 nullptr의 사용은 필수적임

class NullPtr
{
    public:
        //어떤 타입의 포인터와도 치환 가능
        template<typename T>
        operator T* () const
        {
            return 0;
        }
        // 어떤 타입의 멤버 포인터와도 치환 가능
        template<typename C, typename T>
        operator T C::* () const
        {
            return 0;
        }
    private:
        // 주소값 &을 막음
        vodi operator&() const;
};

using

typedef vector<int>::iterator Vecit;

typedef int id;
using id2 = int;

typedef void(*MyFunc)();
using MyFunc2 = void(*)();

template<typename T>
typedef std::vector<T> List; // 불가능함

template<typename T>
using List2 = std::list<T> // 가능함

usingtypedef보다 직관성이 좋음
usingtypedef와는 달리 템플릿을 이용할 수 있음
기본적으로 typedef보다 using이 모든 면에서 뛰어남


enum class

enum PlayerType
{
    PT_Knight,
    PT_Archer,
    PT_Mage
};

double value = PT_Knight;

enum의 경우 unscoped enum으로 enum에 설정되어있는 이름은 전역 사용 범위를 가지고있음
암묵적인 변환이 가능함

enum class ObjectType
{
    Player,
    Monster,
    Projectile
};

double value = ObjectType::Plyaer; // 불가능

enum class의 경우 scoped enum으로 설정되어있는 이름이 영역 안에서만 유효함
암묵적인 변환이 불가능함


delete (삭제된 함수)

class Knight
{
    public:

    private:
        void operator=(const Knight* k); // 사용은 가능하나 문제가있음
        void operator=(const Knight& k) = delete;
    private:
        int _hp = 100;
}

기존에는 사용하지 않는 함수를 private에 정의되지 않은 함수로 만들어줌
그러나 private에 정의하더라도 꺼내서 쓸 수 있는 경우가 발생할 수 있음
private에 선언한 함수의 구현부를 만들지 않을 시에는 함수의 사용을 방지할 수 있으나, 문제를 조기에 발견할 수 없음

함수 뒤에 = delete를 붙여줄 경우 더이상 사용하지 않을 함수라는 것을 확실하게 나타낼 수 있음


override, final

class Player
{
    public:
        virtual void Attack()
        {
            ...
        }
};

class Knight : public Player
{
    public:
        // 재정의(override)
        virtual void Attack() override
        {
            ...
        }
        // 오버로딩(overloading) : 함수 이름 재사용
        void Attack(int a)
        {
            ...
        }
};

int main()
{
    Knight* knight = new Knight();
    knight->Attack();

    Player* player = new Knight();
    player->Attack();

    return 0;
}

virtual로 지정한 함수의 최초 사용이 어디였는지를 알 수 없음
따라서 재정의한 함수라는 것을 나타태기 위해 함수 뒤에 override 키워드를 붙여 사용할 수 있음

마지막으로 사용될 함수의 경우 final을 붙여 더이상 재정의되지 않도록 만들 수 있음


오른값 참조(rvalue reference)

lvalue(왼값) : 단일식을 넘어서 계속 지속되는 개체
rvalue(오른값) : lavlue가 아닌 나머지 (임시 값, 열거형, 람다, i++ 등)

class Pet
{

};

class Knight
{
    public:
        Knight()
        {
            cout << "Knight()" << endl;
        }
        Knight(const Kngiht& knight)
        {
            cout << "const Knight&" << endl;
        }
        ~Knight()
        {
            if (_pet)
                delete _pet;
        }

        // 이동 생성자
        Knight(Knight&& knight)
        {

        }

        void operator=(const Knight& knight)
        {
            cout << "operator=(const Knight&)" << endl;
            _hp = knight._hp;
            if (knight._pet)
                _pet = new Pet(*knight._pet);
        }

        // 이동 대입 연산자
        void operator=(Knight&& knight) noexcept
        {
            cout << "operator(Knight&&)" << endl;
            _hp = knight._hp;
            _pet = knight._pet;
            knight._pet = nullptr;
        }
    public:
        int _hp = 100;
        Pet* _pet = nullptr;
};

void TestKnight_Copy(Knight knight) {}
void TestKnight_LValueRef(Knight& knight) {} 
void TestKnight_ConstLValueRef(const Knight& knight) {}
void TestKnight_RValueRef(Knight&& knight) {} // 이동 대상

int main()
{
    Knight k1;
    TestKnight_Copy(k1);
    TestKnight_LValueRef(k1);
    TestKnight_LValueRef(Knight()); // Knight()로 생성된 객체는 Lvalue가 아니므로 불가능
    TestKnight_ConstLValueRef(Knight()); // const가 붙은 rvalue는 가능

    TestKnight_RValueRef(k1); // lvalue는 불가능
    TestKnight_RvalueRef(Knight()); // rvalue는 가능
    TestKnight_RvalueRef(static_cast<Knight&&>(k1)); 

    Knight k2;
    k2._pet = new Pet();
    k2._hp = 1000;

    Knight k3;
    k3 = static_cast<Kngiht&&>(k2);
    k3 = std::move(k2); // 오른값 참조로 캐스팅, 이름 후보중 하나가 rvalue_cast

    std::unique_ptr<Knight> uptr = std::make_unique<Knight>();
    std::unique_ptr<Knight> uptr2 = std::move(uptr);

    return 0;
}

rvalue로 참조할시 원본 데이터가 유지되지 않아도 됨
rvalue로 참조하는 것은 원본은 날려도 된다는 힌트를 주는 쪽에 가까움
따라서 이동시 얕은 복사도 사용 가능


전달 참조(forwarding reference)

원래는 보편 참조(universal reference) 라는 이름을 가지고있었음

class Knight
{
    public:
        Knight() { cout << "기본 생성자" << endl; }
        Knight(const Knight&) { cout << "복사 생성자" << endl; }
        Knight(Knight&&) noexcept { cout << "이동 생성자" << endl; }
}

void TestRValueRef(Knight&& k)
{
}

void Test_Copy(Knight k)
{
}

template<typename T>
void Test_ForwardingRef(T&& param)
{
    // 왼값 참조일 경우 복사
    // 오른값 참조일 경우 이동
    Test_Copy(std::forward<T>(param));
}

int main()
{
    Knight k1;
    Test_RvalueRef(std::move(k1)); // rvalue_cast

    test_ForwardingRef(k1); // lvalue
    test_ForwardingRef(std::move(k1)); // rvalue

    auto&& k2 = k1; // lvalue
    auto&& k3 = std::move(k1); // rvalue

    Knight k;
    Knight& k4 = k; // lvalue
    Knight&& k5 = std::move(k); // rvalue
    Test_RvalueRef(k5); // 오른값 참조를 참조 - 불가능
    Test_RvalueRef(std::move(k5)); // 오른값 참조 - 가능
}

전달 참조는 형식 연역(type deduction)이 일어날때만 발생
lvalue를 넣어주면 lvalue 참조, rvalue를 넣어주면 rvalue 참조함
함수의 케이스를 줄일 수 있어 편리함
단, 이 때 lvaluervalue 둘 중 어느 것을 받더라도 같은 동작을 해야함

오른값 : 왼값이 아님 = 단일식에서 벗어나면 사용할 수 없음
오른값 참조 : 오른값만 참조할 수 있는 참조 타입


람다(lambda)

함수 객체를 빠르게 만드는 문법
람다 자체가 C++11의 새로운 기능은 아님

enum class ItmeType
{
    None,
    Armor,
    Weapon,
    Jewelry,
    Consumable
};

enum class Rarity
{
    Common,
    Rare,
    Unique
};

class Item
{
    public:
        Item() {}
        Item(int itemid, Rarity rarity, ItemType type)
            : _itemid(itemid), _rarity(rarity), _type(type) {}
    public:
        int _itemid = 0;
        Rarity _rarity = Rarity::Common;
        ItemType _type = ItemType::None;
};

int main()
{
    vector<Item> v;
    v.push_back(Item(1, Rarity::Common, ItemType::Weapon));
    v.push_back(Item(2, Rarity::Common, ItemType::Armor));
    v.push_back(Item(3, Rarity::Rare, ItemType::Jewelry));
    v.push_back(Item(4, Rarity::Unique, ItemType::Weapon));

    {
        struct IsUniqueItem
        {
            FindItemByItemid(int itemid) : _itemid(itemid)
            {

            }

            bool operator()(Item& item)
            {
                return item._rarity == Rarity::Unique;
            }

            int _itemid;
        };
        auto findit = std::find_if(v.begin(), v.end(), IsUniqueItem());

        int itemid = 4;
        auto findit = std::find_if(v.begin(), v.end(), FindItemByItemid(itemid);

        if (findit != v.end())
            cout << "아이템 ID : " << findit->_itmeid << endl;
    }

    {
        // 람다 표현식(lambda expression)
        // 클로저 (closure) : 람다에 의해 만들어진 실행시점 객체
        auto isUniqueLambda = [](Item& item)
        {
            return item._rarity == Rarity::Unique;
        };
        auto findit = std::find_if(v.begin(), v.end(), 
            [](Item& item) { return item._rarity == Rarity::Unique; });

        auto findByItemidLambda = [=](Item& item) { return Item.itemid == _itemid; };
        auto findit = std::find_it(v.begin(), v.end(), findByItemidLambda);

        if (findit != v.end())
            cout << "아이템 ID : " << findit->_itmeid << endl;
    }

    {
        class Knight
        {
            public:
                auto ResetHpJob()
                {
                    auto f = [this]()
                    {
                        this->_hp = 200;
                    };
                    return f;
                }
            public:
                int _hp = 100;
        }

        Knight* k = new Knight();
        auto job = k->ResetHpJob();
        delete k;
        job(); // 메모리 오염 발생
    }
}

[캡처](인자값) { 구현부 }의 형태로 사용
[] : 캡처(capture), 함수 객체 내부에 변수를 저장하는 개념과 유사

  • 기본 캡처 모드는 값(복사) 방식(=)과 참조 방식(&) 중에 선택할 수 있음
  • 변수마다 캡처 모드를 지정해서 사용하는 것도 가능함
  • 캡처 모드를 일괄적으로 설정하는 것을 지양해야함

스마트 포인터(smart pointer)

class Knight
{
    public:
        Knight() { cout << "Knight 생성" << endl; }
        ~Knight() { cout << "Knight 소멸" << endl; }

        void Attack()
        {
            if (_target)
            {
                _target->_hp -= _damage;
                cout << "Hp : " << _target->_hp << endl;
            }
        }

    public:
        int _hp = 100;
        int _damage = 10;
        Knight* _target = nullptr;
};

int main()
{
    Knight* k1 = new Knight();
    Knight* k2 = new Knight();

    k1->_target = k2;
    delete k2;
    k1->Attack(); // 크래시는 발생하지 않으나 이미 해제된 메모리를 참조하여 엉뚱한 결과가 발생함

    return 0;
}
class RefCountBlock
{
    public:
        int _refCount = 1;
};

template<typename T>
class SharedPtr
{
    public:
        SharedPtr() {}
        SharedPtr(T* ptr) : _ptr(ptr)
        {
            if (_ptr != nullptr)
            {
                _block = new RefCountBlock();
                cout << "RefCount : " << _block->_refCount << endl;
            }
        }

        SharedPtr(const SharedPtr* sptr) : _ptr(sptr._ptr), _block(sptr._block)
        {
            if (_ptr != nullptr)
            {
                _block->refCount++;
                cout << "RefCount : " << _block->_refCount << endl;
            }
        }

        void operator=(const SharedPtr& sptr)
        {
            _ptr = sptr._ptr;
            _block = sptr._block;
            if (_ptr != nullptr)
            {
                _block->refCount++;
                cout << "RefCount : " << _block->_refCount << endl;
            }
        }

        ~SharedPtr()
        {
            if (_ptr != nullptr)
            {
                _block->_refCount--;
                cout << "RefCount : " << _block->_refCount << endl;
                if (_block->_refCount == 0)
                {
                    delete _ptr;
                    delete _block;
                    cout << "Delete data" << endl;
                }
            }
        }
    public:
        T* _ptr = nullptr;
        RefCountBlock* _block = nullptr;
}

int main()
{
    SharedPtr<Knight> k2;

    {
        SharedPtr<Knight> k1(new Knight());
        k2 = k1;
    }
}
class Knight
{
    public:
        Knight() { cout << "Knight 생성" << endl; }
        ~Knight() { cout << "Knight 소멸" << endl; }

        void Attack()
        {
            if (_target)
            {
                _target->_hp -= _damage;
                cout << "Hp : " << _target->_hp << endl;
            }
        }

    public:
        int _hp = 100;
        int _damage = 10;
        shared_ptr<Knight>() _target = nullptr;
};

int main()
{
    shared_ptr<Knight> k1 = make_shared<Kngiht>();
    {
        shared_ptr<Knight> k2 = make_shared<Kngiht>();
        k1->_target = k2;
    }

    k1->Attack();

    return 0;
}

스마트 포인터 : 포인터를 알맞은 정책에 따라 관리하는 객체 (포인터를 래핑해서 사용)

  • shared_ptr, weak_ptr, unique_ptr 존재
  • Modern C++에서는 기존 포인터는 거의 사용하지 않는다고 보면 됨
int main()
{
    shared_ptr<Knight> k1 = make_shared<Kngiht>();
    {
        shared_ptr<Knight> k2 = make_shared<Kngiht>();
        k1->_target = k2;
        k2->_target = k1;
    }
    k1->Attack();

    return 0;
}

순환구조일 경우 shared_ptr에서는 문제가 발생할 수 있음
이 경우 weakcount를 가지고있는 weak_ptr을 활용할 수 있음
단, weak_ptr은 직접적으로 메모리 해제에 관여하지 않으므로 명시적으로 메모리 체크 후 shared_ptr로 변환하는 과정이 요구됨

unique_ptr은 일반적인 복사를 막아 다른 곳에 넘겨줄 수 없음

'개인공부 > Rookiss C++ 프로그래밍 입문' 카테고리의 다른 글

Chapter 11 - STL  (0) 2023.03.29
Chapter 10 - 콜백 함수  (0) 2023.03.29
Chapter 9 - 디버깅  (0) 2023.03.29
Chatper 8 - 실습  (0) 2023.03.29
Chapter 7 - 동적 할당  (0) 2023.03.29

댓글