[Advanced C++] 31. struct 추가내용, struct 크기 및 정렬(padding), struct 멤버 접근, class template, std::pair, CTAD, Alias template

dev.kelvin·2025년 3월 27일
1

Advanced C++

목록 보기
31/74
post-thumbnail

1. struct 추가 내용

C++에서는 struct와 class의 멤버로 다른 사용자 정의 타입을 사용할 수 있다

	struct Monster
    {
    	int hp{ };
        int mp{ };
    }
    
    struct GameCharacter
    {
    	Monster M{ };
    }

또한 구조체 내부에서 다른 구조체를 정의하여 다른 사용자 정의 타입을 사용할 수도 있다

	struct GameCharacter
    {
    	struct Monster
        {
        	int hp{ };
            int mp{ };
        }
        
        int temp{ };
        
        Monster M{ };
    }

struct나 class에서는 데이터를 직접 소유하거나 참조할 수 있다

구조체나 클래스가 데이터를 직접 소유하면 구조체나 클래스의 생명주기동안 데이터가 유효하고 멤버 데이터의 값이 예상치 않게 변경될 위험이 줄어든다, 만약 클래스 멤버가 포인터나 참조로 viewer라면 참조하는 데이터가 소멸되면 해당 구조체나 클래스는 더 이상 유효하지 않은 데이터(dangling)를 참조하여 의도치 않은 동작이 발생할 수 있다 (따라서 nullcheck를 꼭 잘해야 한다)

    struct Owner
    {
        std::string name{}; // std::string으로 데이터를 직접 소유함
    };

    struct Viewer
    {
        std::string_view name{}; // std::string_view으로 데이터를 참조
    };

    std::string getName()
    {
        std::cout << "Enter a name: ";
        std::string name{};
        std::cin >> name;
        return name;
    }

    int main()
    {
        Owner o{ getName() };
        std::cout << "The owner's name is " << o.name << '\n';  // 정상 작동

        Viewer v{ getName() };
        std::cout << "The viewer's name is " << v.name << '\n'; // 정의되지 않은 동작 발생 가능 (getName()으로 나온 임시객체를 참조하기 때문, 임시객체는 해당 라인 다음에 소멸되기 때문이다 (v.name은 dangling 상태))

        return 0;
    }

하지만 멤버 데이터를 참조로 들고있으면 클래스 사이즈가 작아져 성능상 유리할 수 있다, 또한 프로그래밍을 하다보면 해당 struct나 class가 참조해야할 일이 생긴다

멤버 데이터를 참조, 포인터로 선언하게 되면 viewer가 되고 그렇지 않으면 owner가 되는것이다
ex) std::string, std::string_view

구조체의 크기 및 정렬

구조체의 크기는 보통 멤버 데이터의 크기의 총합을 예상하지만 항상 그렇지는 않다

	struct Foo
    {
        short a{};  // 2 bytes
        int b{};    // 4 bytes
        double c{}; // 8 bytes
    };

    int main()
    {
        std::cout << sizeof(short) << "\n";
        std::cout << sizeof(int) << "\n";
        std::cout << sizeof(double) << "\n";
        std::cout << sizeof(Foo) << "\n";

        return 0;
    }

이때 Foo의 크기는 16이 나오게된다, 분명 2 + 4 + 8로 14byte를 예상했는데 왜일까?

이는 CPU가 데이터를 효율적으로 접근하도록 컴파일러가 데이터를 정렬했기 때문이다

CPU는 메모리에 접근해서 데이터를 읽을때 1Word 단위로 읽는다, 따라서 32bit운영체제인 경우에는 4byte씩, 64bit운영체제인 경우 8byte씩 읽게 된다

위의 예시로 한번 살펴보자

p는 padding을 의미한다

(a)(a)(b)(b)(b)(b)(p)(p) (c)(c)(c)(c)(c)(c)(c)(c) -> 따라서 16byte가 나오는 것이다

따라서 크기가 큰 순서대로 정렬하면 이러한 padding을 줄일 수 있다


2. 포인터, 참조를 이용하여 구조체 멤버 접근

구조체의 멤버 데이터는 구조체 타입 객체에서 .을 이용하여 접근 할 수 있다

	struct Foo
    {
    	int a{ 10 };
        int b{ 20 };
    };
    
    Foo foo{};
    foo.a; //멤버 데이터 a에 접근

참조 타입은 객체 그 자체처럼 동작하기 때문에 .으로 내부 멤버 데이터에 접근이 가능하다

	void FooFunction(const Foo& InFoo)
    {
		InFoo.a; //멤버 데이터 a에 접근
	}

하지만 포인터 타입은 .으로 멤버 데이터에 접근하기 굉장히 불편하다, 따라서 ->를 이용하여 멤버 데이터에 접근한다

	Foo foo{};
	Foo* ptrFoo{ &foo };
    
    (*ptrFoo).a; //불편
    ptrFoo->a; //편리

->는 포인터를 자동으로 역참조()하고 멤버에 접근하게 해준다, ->가 .보다 연산자 우선순위 걱정도 할 필요가 없어서 더 편리하다

또한 구조체 멤버 데이터가 포인터라면 연쇄적으로 ->를 사용할 수 있다 (하지만 권장하지 않는다, 함수를 만들어서 getter를 반환 후 ->는 한번만 사용하는게 좋다고 생각함)

물론 포인터 타입이 아니면 .으로 연쇄적으로 접근이 가능하다

	struct Temp
    {
        int t{ };
    };

    struct Foo
    {
        Temp* tem{ };
    };

    int main()
    {
        Foo foo{};
        Foo* ptrF{ &foo }; //멤버 데이터 a에 접근
        ptrF->tem->t; //사용 가능은 하지만 권장하지 않음, getter를 만들어서 함수 호출 후 ->를 한번만 사용하여 접근하자
    }

3. class template

class template

함수에서 다양한 데이터 타입을 처리하기 위한 해결책으로 함수 template을 사용할 수 있다
(template을 사용하지 않으면 사용할 모든 타입들에 맞는 함수 오버로딩이 필요하다)

	template <typename T>
    T foo(T a, T b)
    {
    	return (a < b) ? b : a;
    }
    
    foo<int>(10, 30);
    foo(10, 30);

이와 비슷한 상황이 struct에도 나오게 된다

	struct Foo
    {
    	int a{ };
        int b{ }
    };
    
    constexpr int max(Foo f)
    {
        return (f.a < f.b ? f.b : f.a);
    }
    
    int main()
    {
    	Foo f{10, 20};
        max(Foo); //20
    }

위 코드에서는 int 타입만 지원이 가능한 상태인데 double 타입도 지원하고 싶다면? struct의 멤버 데이터 타입이 double인 struct와 함수를 또 만들어야 한다

또한 함수의 반환 타입만 다른경우 오버로딩이 불가능하며 코드의 중복이 굉장히 많아지는 아주 보기 안좋은 코드 형식이 된다

그리고 같은 이름의 구조체로 멤버 데이터 타입만 다르게 정의가 불가능하다 (중복 정의)

이럴때 함수 템플릿과 같은 방식으로 class template을 사용하면 된다

	template <typename T>
    struct Foo
    {
    	T a{ };
        T b{ };
    };
    
    int main()
    {
    	Foo<int> intF{10, 20};
        Foo<double> doubleF{10.0, 20.0};
    }

이렇게 각 타입별 struct 인스턴스를 생성할 수 있다

위의 함수와 연결해서 사용해보자

	template <typename T>
    struct Foo
    {
    	T a{ };
        T b{ };
    };
    
    template <typename T>
    constexpr T max(Foo<T> f)
    {
        return (f.a < f.b ? f.b : f.a);
    }
    
    Foo<int> intF{10, 20};
    Foo<double> doubleF{10.0, 20.0};
        
    max<int>(intF);
    max<double>(doubleF);

이렇게 함수와 struct를 템플릿화 해서 다양한 타입에 대응할 수 있다

함수 템플릿과 마찬가지로 여러개의 템플릿 타입을 사용할 수 있다

	template <typename T, typename U>
    struct Foo
    {
    	T a{ };
        U b{ };
    };
    
    Foo<T, U> f; //이런식으로 여러개의 템플릿 타입 인스턴스화가 가능하다
    Foo<int, double> foo{};

std::pair

위와 같은 두 개의 다른 데이터를 하나로 담는 구조체 타입인 std::pair가 존재한다, 이는 위에서 정리한 템플릿 구조체로 C++ standard library에 이미 구현이 되어있다

	std::pair<타입, 타입> 이름{ 값, 값 };
    std::pair<int, float> p1{1, 2.f};
    
    p1.first; //1번째 값
    p1.second; //2번째 값

4. class template 타입 추론(CTAD)

CTAD

C++17부터 class template 객체를 인스턴스화 할 때 컴파일러는 초기화된 값으로 템플릿 타입을 추론할 수 있다, 이러한 추론을 CTAD (Class Template Argument Deduction)이라고 한다

	std::pair<int, int> p1{1, 2}; //명시적으로 클래스 템플릿 std::pair<int, int>를 지정
    std::pair p2{1, 2}; //CTAD로 컴파일러가 std::pair<int, int>를 추론한 것

단 타입이 없지만<>로 되어 있거나 갯수가 다르게 타입이 기입되어 있다면 CTAD가 수행되지 않아 에러가 발생한다

	std::pair<> p1{1, 2}; //error
    std::pair<int> p2{1, 2}; //error

CTAD는 타입 추론이기 때문에 초기화 값에 literal 접미사를 붙여 타입 추론을 할 수 있다

	std::pair p1{1.f, 2.f}; //std::pair<float, float>으로 추론
    std::pair p2{1u, 2u}; //std::pair<unsigned int, unsigned int>로 추론

이때 C++17에서는 class template타입 구조체는 CTAD로 바로 추론이 불가능하다

	template <typename T, typename U>
    struct Foo
    {
    	T first{ };
        U second{ };
    };
    
    int main()
    {
    	Foo<int, int> F1{ 1, 2 }; //ok
        Foo F2{ 1, 2 }; //추론 불가 (C++17), C++20에서는 가능
    }

따라서 C++17에서는 추론 가이드가 필요하다

다음과 같이 추론 가이드 작성이 가능하다

	template <typename T, typename U>
    struct Foo
    {
    	T first{ };
        U second{ };
    };
    
	template <typename T, typename U>
    Foo(T, U) -> Foo<T, U>; //추론 가이드 (동일한 템플릿 타입 정의를 사용한다)
    
    int main()
    {
        Foo F2{ 1, 2 };
    }

Foo(T, U) -> Foo<T, U>를 통해 컴파일러가 Foo F2{ 1, 2 };를 보고 Foo<int, int>를 추론할 수 있게 된 것이다

정리하면 T, U타입으로 초기화된 Foo타입 객체를 Foo< T, U >로 추론하라고 컴파일러에게 지시하는것이다

그렇다면 단일 템플릿 타입이라면?

	template <typename T>
    struct Foo
    {
    	T first{ };
    };
    
    template <typename T>
    Foo(T) -> Foo<T>; //추론 가이드 (동일한 템플릿 타입 정의를 사용한다)
    
    int main()
    {
    	Foo F1{10};
    }

같은 방식으로 사용하면 된다

정리하면 T타입으로 초기화된 Foo타입 객체를 Foo< T >로 추론하라고 컴파일러에게 지시하는것이다

하지만 C++20에서는 이러한 class template 타입 구조체를 추론할 때 자동으로 추론 가이드를 생성하는 기능이 추가되었기 때문에 위와 같은 코드는 작성하지 않아도 된다

(std::pair는 같은 class template타입 구조체이지만 미리 C++에서 정의된 추론 가이드가 있기 때문에 타입을 작성하지 않아도 자동 추론이 된 것임)

class template default type

이러한 class template에도 함수의 매개변수처럼 default 값을 지정할 수 있다

	template <typename T=int, typename U=float>
    struct Foo
    {
    	T first{};
        U second{};
    };
    
    template <typename T, typename U>
    Foo(T, U) -> Foo<T, U>;
    
    Foo F1{}; //default타입인 <int, float>이 된다
    Foo F1{1, 2}; //CTAD가 <int, int>로 추론
    Foo<int, int> F1{1, 2}; //명시적으로 <int, int>가 된다

클래스 타입 내부의 멤버 초기화에는 CTAD가 작동하지 않는다

	struct Foo
    {
    	std::pair p1{1, 2}; //error, CTAD 작동X
    };

그리고 함수 매개변수에서는 CTAD가 작동하지 않는다

    void print(std::pair p) //error, CTAD 작동X
    {
        std::cout << p.first << ' ' << p.second << '\n';
    }

따라서 템플릿을 사용하는게 좋다

	template <typename T, typename U>
    void print(std::pair<T, U> p)
    {
        std::cout << p.first << ' ' << p.second << '\n';
    }
    
    int main()
    {
        std::pair p { 1, 2 }; // p는 std::pair<int, int>로 추론됨
        print(p);

        return 0;
    }

5. Alias template

Alias template

using이나 typedef로 타입 별칭을 만들어 기존 타입에 대한 별칭을 정의할 수 있다

template으로 되어있는 struct도 이러한 타입별칭을 만들 수 있다

	template <typename T>
    struct Foo
    {
    	T one{};
        T two{};
    };
    
    template <typename T>
    void print(const Foo<T>& f)
    {
        std::cout << f.one << ' ' << f.two << '\n';
    }
    
    using typedefFoo = Foo<int>; //Foo<int> 타입 별칭 선언
    typedefFoo p{ 1, 2 }; //Foo<int> 대신 타입 별칭 사용

이런 template으로 되어있는 struct 타입 별칭은 전역/지역 둘 다 정의할 수 있다

이러한 template으로 되어있는 struct 타입 별칭은 프로그래머가 타입을 하나하나 지정해서 using에 사용해야 한다

그렇다면 < T > template으로 되어있는 struct를 별칭으로 만들고 싶다면 어떻게 해야할까?

	template <typename T>
    struct Foo
    {
    	T one{};
        T two{};
    };
    
    template <typename T>
    using TypenameFoo = Foo<T>; //TypenameFoo가 Alias template이 된다
    
    int main()
    {
    	TypenameFoo TF1 { 1, 2 }; //TypenameFoo<T>는 Foo<T>의 타입 별칭이 된다, C++20이후로 CTAD가 적용이 가능하여 초기화 값에서 타입을 자동으로 유추한다 (단 함수 매개변수에는 CTAD적용이 안되기 때문에 <T>를 명시해줘야 한다
       	TypenameFoo<double> TF2 { 1.5, 2.5 }; //C++20전에는 타입을 명시적으로 지정했어야 했다
    }

이러한 < T > template으로 되어있는 struct를 별칭으로 만들기 위해서는 반드시 전역에 정의해야 한다

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글