I'm FanJae.

[C++ Intermediate] Trivial 본문

C++/Intermediate

[C++ Intermediate] Trivial

FanJae 2024. 9. 19. 22:12

1. Trivial

 

1-1. Special Member Function

- 사용자가 제공하지 않으면 컴파일러가 제공하는 멤버 함수가 존재한다.

① 디폴트 생성자(Default Constructor)

② 소멸자(Destructor)

③ 복사 생성자(Copy Constructor)

④ 복사 대입연산자(Copy Assignment)

⑤ 이동 생성자(Move Constructor)

⑥ 이동 대입연산자(Move Assignment)

 

※ 보통 이러한 멤버 함수들을 Trivial 하다고 한다.


2. Trivial Default Constructor

※ 예제가 아주 많아서 check 함수는 한번만 보이고, 이후 부터는 임의 생략한다.

① The constructor is not user-provided
② T has no virtual member functions
③ T has no virtual base classes
④ T has no non-static members with default initializers.
⑤ Every direct base of T has a trivial default constructor
⑥ Every non-static member of class has a trivial default constructor

 

- 디폴트 생성자가 Trivial 하다는 의미는 컴파일러가 생성하는 디폴트 생성자가 아무일도 하지 않는 경우이다.

- 조사하는 방법은 type_traits를 사용하면 된다.

#include <iostream>
#include <type_traits>

struct TrivialDefaultCtor	 // true	
{
	int data;
};

struct NonTrivialDefaultCtor
{
    int data;
    NonTrivialDefaultCtor() {}
};

template<class T> void check()
{
    std::cout << typeid(T).name() << " : ";
    std::cout << std::boolalpha;
    std::cout << std::is_trivially_default_constructible_v<T> << std::endl;
}
int main()
{
    check<TrivialDefaultCtor>(); // True
    check<NonTrivialDefaultCtor>(); // False
}

- TrivialDefaultCtor은 컴파일러가 Default 생성자를 만들고 아무것도 안한다. 따라서 True가 나온다.

- 반면, NonTrivialDefaultCtor은 사용자가 만들었기 때문에 False가 된다.

 

struct Type1
{
	// 컴파일러가 디폴트 생성자 제공안함. - false
	Type1(int a) {}
};

struct Type2
{
	Type2() {};			// false
	Type2(int a) {}
};

struct Type3
{
	Type3() = default;	// true
	Type3(int a) {}
};

struct Type4		// false
{
	int data = 0;

	//
//	int data;
//	Type4() : data(0) {}
};

- Type1의 경우는 컴파일러가 별도의 디폴트 생성자를 제공 안한다. 따라서 False이다.

- Type2의 경우는 사용자가 만들었기 때문에 False이다.

- Type3의 경우는 default 생성자를 사용자에게 만들어달라고 요구했고, 아무것도 안한다. 따라서 True이다.

 

struct Type4		// false
{
	int data = 0;

//  위와 같이 만들면 아래와 같이 코드를 변경한다.
//	int data;
//	Type4() : data(0) {}
};

struct Type5  // false
{
	int data;
	virtual void foo() {}
};

struct Type6 // false 
{
	int data1;
	NonTrivialDefaultCtor data2;

	//	Type6() : data2() {} // 컴파일러가 추가한 코드
};

- Type4의 경우는 초기화 과정에서 변환이 일어난다. 초기화 때문에 trivial 하지 않다.

- Type5의 경우는 가상 함수가 있는 경우 가상 함수 테이블을 초기화 하는 작업이 있다. 따라서, trivial 하지 않다.

- Type6의 경우는 trival 하지 않은 멤버 데이터가 있다. 따라서 trivial 하지 않다.

 

struct Type7 // true
{
	int data1;
	TrivialDefaultCtor data2;
	//	Type7() : data2() {} // 컴파일러가 추가한 코드

};

// false 
struct Type8 : public NonTrivialDefaultCtor
{
	int data;
};


// true
struct Type9 : public TrivialDefaultCtor
{
	int data;
};

- Type7의 경우는 멤버 데이터가 Trivial 하다. 컴파일러가 추가한 생성자에서는 아무것도 하지 않는다. 따라서 trivial 하다.

- Type8의 경우는 기반 클래스의 생성자를 호출 하지만, 이것이 trivial하지 않아서 trivial하지 않다.

- Type9의 경우는 기반 클래스의 생성자를 호출  하지만, 이는 trivial이다. 따라서 trivial하다.

 

// false
struct Type10 : public virtual TrivialDefaultCtor
{
};

// false
struct Type11
{
	Type11() = delete;
};

// false
struct Type12
{
	int& ref; // 참조 멤버가 있으면
	// 디폴트 생성자는 =delete됨.
};

// false
struct Type13
{
	const int c;
};

- Type10의 경우 trivial 한 Default 생성자를 가진 것을 상속 받지만 Virtual 상속이다. 

- 이 경우, 가상 상속을 위한 초기화 구문이 필요하기 때문에 Type10은 trivial 하지 않다.

- Type11의 경우, default 생성자를 임의로 삭제했다. 컴파일러가 생성하는 생성자없기에 trivial 하지 않다.

- Type12의 경우, 초기값이 없는 참조는 만들 수 없다. 참조가 멤버로 있으면 디폴트 생성자는 delete 된다.

- 따라서, Type12도 trivial 하지 않다.

- Type13의 경우, 상수 멤버가 있다. 초기화 되지 않은 상수가 만들어지기 때문에, Default 생성자가 삭제된다.

- Type12와 비슷한 이유로 trivial하지 않다.

 

1-3. Assembly Level

- 생성자 호출을 확인할 수 있다.

 

- 하지만 이와 같이 생성자를 삭제할 경우, main 에서 생성자를 호출 하는 부분이 사라지는 것을 확인할 수 있다.

- default 와 같이 적었을 때도 결과는 동일하게 나오는 것을 확인할 수 있다.

- 가상 함수를 만들었을때는 다음과 같이 생성자의 호출을 확인 할 수 있다.

 

※ C++ 표준에서 언급되는 얘기는 아니다. 최적화 옵션이 들어가면 달라질 수 있는 사항이다.


3. Trivial Copy Constructor

#include <iostream>
#include <string>
#include <type_traits>

class Point
{
	int x;
	int y;
public:
	Point() = default;
	Point(int a, int b) : x(a), y(b) {}
};

int main()
{
	std::cout << std::is_trivially_copy_constructible_v<Point> << std::endl;

	Point pt1(1,2);
	Point pt2 = pt1;

	Point pt3;
	memmove(&pt3, &pt1, sizeof(Point));
}

- 컴파일러가 제공하는 복사 생성자가 메모리 복사 이외 어떤 일도 수행하지 않으면 Trivial 하다고 한다.

- 사용자가 직접 객체를 memmove 등으로 복사하는 것과 동일하다.

 

"effectively copies every scalar subobject (including, recursively, subobject of subobjects and so forth) of the argument and performs no other action"
① It is not user-provided (that is, it is implicitly-defined or defaulted)
② T has no virtual member functions
③ T has no virtual base classes
④ the copy constructor selected for every direct base of T is trivial;
⑤ the copy constructor selected for every non-static class type (or array of class type) member of T is trivial

 

3-1. Trivial 복사 생성자의 활용 예시

#include <iostream>
#include <type_traits>

struct Point
{
    int x = 0;
    int y = 0;
};

template<class T>
void copy_type(T* dst, T* src, std::size_t sz)
{
}

int main()
{
    Point arr1[5];
    Point arr2[5];
    
    copy_type(arr1, arr2, 5);
}

- T의 복사 생성자가 trivial 하다면, 배열  전체를 memcpy나 memmove 등으로 복사하는 것이 빠르다.

- T의 복사 생성자가 trivial 하지 않다면, 배열의 모든 요소에 대해 하나씩 복사 생성자를 호출해서 복사해야 한다.

 

#include <iostream>
#include <type_traits>

struct Point
{
    int x = 0;
    int y = 0;
};

template<class T>
void copy_type(T* dst, T* src, std::size_t sz)
{
     if constexpr ( std::is_trivially_copy_constructible_v<T> )
     {
        std::cout << "using memcpy" << std::endl;
        memcpy(dst, src, sizeof(T)*sz);
     }
     else
     {
        std::cout << "using copy ctor" << std::endl;
        while(sz--)
        {
            new(dst) T(*src);
            --dst, --src;
        }
     }
}

int main()
{
    Point arr1[5];
    Point arr2[5];
    
    copy_type(arr1, arr2, 5);
}

- 위와 같은 복사 기법을 많이 사용한다.

Comments