* const

 

C++와 다르게 C# const(constant의 줄임말이다.) 키워드는 변수의 생명 주기동안 변경되지 않는 상수 변수를 위해 사용된다. 선언과 동시에 상수 변수에 값을 할당해야 한다. 선언 시 값이 상수 변수에 할당되고 난 이후에는 변경되지 않는다.

 

상수 변수의 값은 컴파일 타임 값, const 키워드를 통해 선언되는 변수는 컴파일 타임 상수이다.

// C++ constexpr과 비슷한 것 같다.

 

상수 객체를 선언하기 위해 const 키워드를 사용할 수 없다. const 키워드는 기본 데이터 타입(int, float, string 등)에서만 사용 가능하다.

const string constString = "test text";

// 기본 데이터 타입에 대해서만 const사용할 수 있다.
// 클래스 객체는 불가능하다.
// const MyClass constMyClass1 = new MyClass();
            
// null을 할당하는 것은 가능하다.
const MyClass? constMyClass2 = null;

오른쪽에는 상수가 와야하는데 new MyClass()는 상수가 아니기 때문에 불가능하다.

 

 

* readonly

 

- 정의

 

readonly 키워드는 읽기전용 변수를 정의하기 위해 사용된다. 변수는 클래스 스코프나 생성자에서만 값을 할당 받을 수 있다. 다른 곳에서는 readonly 변수의 값을 변경할 수 없다.

 

readonly는 크게 다음과 같은 위치에서 사용될 수 있다.

  1. readonly (필드) : 해당 필드가 선언이나 생성자에서만 할당될 수 있음을 나타낸다.
  2. readonly struct : struct가 불변임을 나타낸다. readonly struct의 모든 필드는 readonly여야 한다.
  3. readonly (struct의 필드) : 해당 struct 필드가 struct의 상태를 변경하지 않음을 나타낸다.
  4. ref readonly (메소드) : 참조 값을 반환하지만 수정할 수 없다.

 

 

- 동작 방식

 

readonly 필드는 생성자 이후에 할당될 수 없다. 값 타입, 참조 타입 마찬가지로 필드의 값은 변경할 수 없다.

class ReadonlyTestClass1
{
    public ReadonlyTestClass1(int input)
    {
        Value = input;
    }

    public readonly int Value;
}

class ReadonlyTestClass2
{
    public ReadonlyTestClass2(string input)
    {
        Value = input;
    }

    public readonly string Value;
}

static void Main(string[] args)
{
    /* 값 타입 */
    ReadonlyTestClass1 readonlyTestClass1 = new ReadonlyTestClass1(3);

    // 변경할 수 없다.
    //readonlyTestClass1.Value = 3;

    //==================================================================================================

    /* 불변 참조 타입 */
    ReadonlyTestClass2 readonlyTestClass2 = new ReadonlyTestClass2("text");
    string tempString = new string("text1");

    // 새로운 문자열을 가리키도록 변경할 수 없다.
    //readonlyTestClass2.Value = tempString;
}

 

ref readonly를 반환하는 경우 다음과 같이 작성하면 된다.

class ReturnRefReadonly
{
    public ref int getValue() { return ref value; }
    public ref readonly int getValueReadonly() { return ref value; }
    private int value = 3;
}

static void Main(string[] args)
{
    ReturnRefReadonly rrr = new ReturnRefReadonly();
    ref int temp1 = ref rrr.getValue();
            
    ref readonly int temp2 = ref rrr.getValueReadonly();
    //temp2 = 5; // readonly라서 불가능하다.
}

 

 

- 주의 사항

 

@ 데이터를 참조하는 값(힙의 메모리 주소 값이라고 생각해도 될 것 같다.)과 실제 데이터의 값

 

위에서 readonly 필드의 값이 변경되지 않음을 알 수 있었다. 하지만 참조 타입의 경우 readonly 필드의 값은 특정 메모리를 가리키는 주소 값 같은 것이기 때문에 문제가 생길 수 있다. 실제 데이터 관점에서 하나씩 따져보면 다음과 같다.

  1. 값 타입 : readonly 변수가 데이터를 직접 포함하고 있기 때문에 데이터를 변경할 수 없다. 데이터가 보호된다.
  2. 불변 참조 타입 :  readonly 변수이기 때문에 항상 동일한 불변 데이터를 참조해야 한다. 불변 데이터이기 때문에 데이터가 보호된다.
  3. 가변 참조 타입 :  readonly 변수이기 때문에 항상 동일한 데이터를 참조해야 한다. 하지만 데이터는 변경될 수 있다.  

 

1, 2번은 데이터가 보호된다. 위에서 확인했다.

하지만 3번 가변 참조 타입일 때는 가리키는 데이터만 변경되지 않을 뿐 실제 내용은 변경될 수 있다.

class ReadonlyTestClass3
{
    public ReadonlyTestClass3(StringBuilder input)
    {
        Value = input;
    }

    public readonly StringBuilder Value;
}

static void Main(string[] args)
{
    /* 참조 타입 */
    ReadonlyTestClass3 readonlyTestClass3 = new ReadonlyTestClass3(new StringBuilder("text"));
    StringBuilder tempStringBuilder = new StringBuilder("text1");

    // 새로운 문자열을 기리키도록 변경할 수 없다.
    //readonlyTestClass3.Value = tempStringBuilder;

    // 하지만 readonly가 적용되는 것은 가리키고 있는 참조 정보이다. 참조 정보만 건드리지 않으면 원본을 수정해도 문제없다.
    tempStringBuilder.Append("text1"); // 컴파일 가능
}

위의 코드에서 필드 Value는 StringBuilder가 할당된 힙 공간을 가리키고 있는 참조 값이다. 해당 힙 공간을 가리키는 사실만 변경하지 않는다면 실제로 데이터가 들어있는 StrinbBuilder의 내용을 바꿔도 상관없다. 이러한 보안 취약점 때문에 MS에서는 가능한 readonly 가변 참조 타입으로 선언하지 말라고 되어있다.

 

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly

 

readonly keyword - C# Reference

Table of contents readonly (C# Reference) Article 06/18/2022 3 minutes to read 14 contributors In this article --> The readonly keyword is a modifier that can be used in four contexts: In a field declaration, readonly indicates that assignment to the field

docs.microsoft.com

 

 

@ C++에서의 해결 방법

 

참고로 C++에서는 이러한 문제점을 막기 위해 가리키는 대상의 불변과 대상의 데이터 불변을 const 키워드를 사용하여 따로 설정할 수 있도록 되어있다. 

string stringValue1("text1");
string stringValue2("text2");

//===============================================================
/* const DataType* 형식 */

const string* constStringPtr = &stringValue1;
	
// 가리키고 있는 대상의 데이터를 수정할 수 없다.
//constStringPtr[0] = 'a';

// 가리키는 대상을 수정할 수 있다.
constStringPtr = &stringValue2;


//===============================================================
/* DataType* const 형식 */
	
string* const stringPtrConst = &stringValue1;
	
// 가리키고 있는 대상의 데이터를 수정할 수 있다.
stringPtrConst[0] = 'a';

// 가리키는 대상을 수정할 수 없다.
//stringPtrConst = &stringValue2;


//===============================================================
/* const DataType* const 형식 */

const string* const constStringConstPtr = &stringValue1;
// 가리키고 있는 대상의 데이터를 수정할 수 없다.
//constStringConstPtr[0] = 'a';

// 가리키는 대상을 수정할 수 없다.
//constStringConstPtr = &stringValue2;

 

 

@ C#에서의 대안

 

C#에서는 C++에서 원본 데이터의 수정을 막아주는 const와 같은 도구가 없다. 이 문제를 해결하기 위해서는 다음과 같은 방법을 생각해볼 수 있다.

 

  • 참조 객체를 반환하는데 원본 객체를 변경 시키지 않으려면 원본을 반환하지 말고 깊은 복사를 통해 새로운 객체를 생성하여 반환한다. 문제점은 단순히 객체를 읽었을 뿐인데 힙 메모리에 새로운 객체가 할당되고 가비지 컬렉터는 이 쓰레기를 처리하기 위해 고생한다는 점이다.
  • 참조 객체를 직접 반환하는 메소드 대신 객체에서 원하는 값만 복사하여 반환하는 메소드를 제공한다.

 

 

* const vs readonly

 

const는 컴파일 타임 상수를 정의할 때 사용한다. 필드의 선언에서만 초기화할 수 있다. 생성자에서 수정할 수 없다.

 

readonly는 필드의 선언과 생성자에서 여러 번 할당 가능하다. 생성자에 따라서 다른 값을 가질 수 있다. 런타임 상수로 사용할 수 있다.

class Const_ReadOnly
{
    public Const_ReadOnly(int value)
    {
        // 변경할 수 없다.
        //ConstValue = value;

        // 생성자에서 얼마든지 변경가능하다.
        ReadonlyValue = value;
        if(0 == value)
        {
            ReadonlyValue = -1;
        }
    }

    public Const_ReadOnly(int value1, int value2)
    {
        ReadonlyValue = value1 % value2;
    }

    public const int ConstValue = 5;
    public readonly int ReadonlyValue = 5;
}

 

'C#' 카테고리의 다른 글

[C#] abstract  (0) 2022.06.28
[C#] abstract class vs interface  (0) 2022.06.28
[C#] ref, out  (0) 2022.06.28
[C#] 강력한 형식 언어(Strongly Typed Language)  (0) 2022.06.28
[C#] var  (0) 2022.06.28

* pow

 

template<int m, int n>
struct MyPow
{
    static const long long result = m * MyPow<m, n - 1>::result;
};

template<int m>
struct MyPow<m, 0>
{
    static const long long result = 1;
};

 

 

* factorial

 

template<int n>
struct MyFactorial
{
	static const long long result = n * MyFactorial<n - 1>::result;
};

template <>
struct MyFactorial<1>
{
	static const long long result = 1;
};

 

 

* enable_if

 

template <bool condition, typename T = void>
struct MyEnableIf {}; // condition이 false일때는 정의되지 않는다.

template <typename T> // condition이 true일때만 타입을 정의한다.
struct MyEnableIf<true, T> { using type = T; }; 

template <typename T>
typename MyEnableIf<std::is_integral<std::remove_reference_t<T>>::value, bool>::type
                Multiply(T&& a, T&& b) { return a * b; }

 

 

 

* is_prime

 

// 나눠 떨어지면 재귀를 빠져나가도록 설계한다.
template <int n, int d>
struct Divisible
{
    static const bool result = (0 == n % d) ? true : Divisible< n, d - 1 >::result;
};

// 2에서 브래이크를 걸어준다.
template <int n>
struct Divisible <n, 2>
{
    static const bool result = (0 == n % 2);
}; 


template <int n>
struct MyIsPrime 
{
    static const bool result = (false == Divisible< n, n / 2 >::result);
};

template <>
struct MyIsPrime<2> // 2는 소수, n / 2 부터 나눠서 확인하기 때문에 예외처리
{
    static const bool result = true;
};

template <>
struct MyIsPrime<3> // 3은 소수, n / 2 부터 나눠서 확인하기 때문에 예외처리
{
    static const bool result = true;
};

 

'C++' 카테고리의 다른 글

템플릿 메타 프로그래밍(Template Meta Programming, TMP)  (0) 2022.05.23
가변인자 템플릿(Variadic Template) 예시  (0) 2022.05.23
가변인자 템플릿(Variadic Template)  (0) 2022.05.23
템플릿(Template)  (0) 2022.05.23
람다(Lambda)  (0) 2022.04.11

* 정의

 

템플릿 메타 프로그래밍(template meta programming, TMP)은 컴파일 도중에 실행되는 템플릿 기반의 프로그램을 작성하는 것을 말한다. 템플릿 기반 프로그램을 작성함으로써 컴파일러에게 프로그램 코드를 생성하도록 하여 컴파일 시점에 많은 것들을 결정하도록 만든다.

 

일반적인 프로그래밍 방식에서 실행 시점에서 대부분 수행한다면 템플릿 메타 프로그래밍 방식에서는 컴파일 시점에 많은 것들을 수행하여 실행 시점에서의 연산을 줄인다.

 

템플릿 메타 프로그램은 변수의 값이 한번 정해지면 변경할 수 없기 때문에 함수형 프로그래밍의 한 형태로 볼 수 있다. 많은 템플릿 구현에서 흐름 제어가 재귀를 통해 수행된다. 유사한 코드 중복을 피하는 일반화 프로그래밍과도 연관있다.

 

 

* 장점

 

템플릿 메타 프로그래밍의 장점, 템플릿의 장점은 다음과 같이 정리할 수 있다.

 

- 컴파일 시점에 연산을 하기 때문에 실행 시점에서 연산을 하거나 함수를 별도로 호출하는 시간을 아낄 수 있다. 또한 컴파일 시간에 확인하기 때문에 문제점을 더 빠르게 파악할 수 있다.

 

- 상수전파(constant propagation)이 더 잘 일어날 수 있다.

여기서 상수전파란 컴파일러의 실행시간 최적화 기법으로 컴파일 시 상수를 포함하는 연산이 계산될 수 있으면 계산하두어 코드를 줄이는 방식이다. 다음과 같은 코드가 있다고 가정할 때 y의 값은 컴파일 시 6으로 계산된다.

x = 3;
y = 2 * x;

 

 

- 정적 다형성을 구현할 수 있다.

동적 다형성(가상 함수)은 알맞은 가상함수를 호출하기 위해 vtable을 확인해서 적절한 함수를 호출하게 되며 이는 오버헤드로 이어진다. 정적 다형성을 구현하면 더 성능이 좋다. 일반적인 경우에는 부모 클래스에서 자식 클래스의 정보를 모르지만 템플릿을 활용하면 가능하다. 다음과 같이 구현된다.

template <typename Super>
class Base
{
public:
    void doSomething(void)
    {
        // ...
        static_cast<Super*>(this)->doSomethingImpl();
        // ...
    }
};

class Derived : public Base<Derived>
{
public:
    void doSomethingImpl(void)
    {
        // ...
    }
};

 

 

- 상수만 사용될 수 있는 문맥에 템플릿 정보를 활용할 수 있다. 예를 들어 배열의 원소 개수를 지정할 때는 무조건 상수의 값을 받을 수 있는데, 이에 템플릿을 사용할 수 있다. 이를 활용하면 힙에 할당하지 않고 스택에서 처리할 수 있다.

// std::array
template <class _Ty, size_t _Size>
class array { // fixed size array of values
public:
// ...
    _Ty _Elems[_Size];
};

 

- 내부 자료형이 확정되지 않아도 클래스나 함수를 만들어내는 것이 가능하다. 일반화 프로그래밍(generic programming)에 특화되어 있다고 보면 된다.

 

- 가변인자를 전달할 수 있으며 일반 가변 함수 매개변수나 매크로와 달리 타입 안정성을 갖출 수 있다.

https://create-new-worlds.tistory.com/303

 

가변인자 템플릿(Variadic Template) 예시

* printf 일반 가변인자 함수와 비교했을 때 가변인자 템플릿은 타입 안정성을 갖출 수 있다. 일반적으로 우리가 사용하는 printf의 경우 %d와 같이 어떤 형식으로 출력해야할 지 정해줘야 한다. 하

create-new-worlds.tistory.com

 

- 암시적 자료 변환을 막기 위해 컨디션에 따라 특정 타입을 선언하는 std::enable_if와 같은 함수를 활용할 수 있다.

std::enable_if는 조건에 따라 해당 타입을 정의할지 정의하지 않을 지 결정하는 기법으로 조건에 맞지 않는다면 컴파일 에러를 발생시키게 된다. 사용 예시는 다음과 같다.

// 정수형 타입이 들어왔을 때만 반환형인 bool이 정상적으로 정의된다.
template <typename T>
typename std::enable_if<std::is_integral<std::remove_reference_t<T>>::value, bool>::type 
is_odd(T&& i) { return bool(i % 2); }


// 두번째 템플릿 인자는 조건 체크를 위해 넣어둔 것이다. 이를 숨기기 위한 두가지 방법이 존재한다.
// 1. 뒤에 = 0을 붙인다. 2. 앞에 typename = 을 붙인다.

// typename = 방식
template<typename T, typename = typename std::enable_if<std::is_integral<std::remove_reference_t<T>>::value, void>::type>
bool is_even(T&& i) { return !bool(i % 2); }

// = 0 방식
template<typename T, typename std::enable_if<std::is_integral<std::remove_reference_t<T>>::value, int>::type = 0>
bool is_even1(T&& i) { return !bool(i % 2); }

 

 

* 단점

 

- 컴파일 시점에 연산해야하는 것들이 많아서 컴파일 시간이 늘어난다.

 

- 특정 소스파일 내부에서만 사용하는 것이 아니라면 항상 헤더 파일에 있어야 한다. 

 

- 통상적인 반복문 대신 재귀 호출로 구현해야 한다.

 

- 일반 코드에 비해 가독성이 떨어진다.

 

- 디버깅하기 매우 어렵다. 오류 메시지를 보고 판단해야하는데 오류 메시지도 템플릿과 연관되어 있기 때문에 너무 많고 복잡하다.

 

- 배포 시 다른 개발자들에게 내부 코드를 강제로 공개하게 된다.

 

- 컴파일러마다 템플릿 사용 방법이 조금씩 다를 수 있어서 이식성에 문제가 있을 수 있다.

 

 

* 예시

 

https://create-new-worlds.tistory.com/307

 

템플릿 메타 프로그래밍(Template Meta Programming, TMP) 예시

* pow template struct MyPow { static const long long result = m * MyPow ::result; }; template struct MyPow { static const long long result = 1; }; * factorial template struct MyFactorial { static c..

create-new-worlds.tistory.com

 

 

 

* 정의

 

가변인자 템플릿(variadic template)은 0개 이상의(고정 개수가 아닌) 변수를 인수로 사용할 수 있는 클래스나 함수 템플릿의 한 종류이다. 간단히 여러 개의 인수를 사용할 수 있는 템플릿이다.

 

가변인자 템플릿과 반대로 일반 템플릿은 선언 시 지정된 고정 개수의 매개변수만 가질 수 있다.

 

 

* 동작 방식

 

- 기본 형태

 

일반적으로 다음과 같이 사용된다.

필자는 직관적으로 이해하기 위해 (타입들... 여러 타입) 을 가진 (여러타입(타입들 ...) ...여러변수)와 같이 이해하였다.

template <typename T, typename... Types>
ReturnType FunctionName(T current, Types... rest)
{
    FunctionName(rest...);
    // ...
}

...으로 표현되는 부분의 의미

  • ...가 왼쪽에 붙는 경우 : 파라미터 팩(parameter pack), 0개 이상의 함수 인자를 나타낸다.
  • ...가 오른쪽에 붙는 경우 : 파라미터 팩을 풀어서 인자로 분리하는 역할을 한다.

 

가변인자들은 인자들을 순회할 수 있고, 인자들의 개수를 알 수 있고, 인덱스를 통해 값에 접근할 수 있으며 템플릿의 인자를 분할할 수 있다.

 

 

- 넘겨줄 수 있는 가변인자들의 개수

 

템플릿에 0개 이상의 가변인자를 넘겨줄 수 있다. 0개도 가능하다. 즉 다음과 같은 형식도 허용된다.

template<typename... Types>
class Tuple { /* ... */ };

Tuple<> emptyTuple;

 

1개이상으로 보장하고 싶다면 다음과 같이 가변인자 템플릿을 정의하면 된다.

template<typename T, typename... Types>
class Tuple_1 { /* ... */ };

// 컴파일 에러가 발생한다.
//Tuple_1<> tuple_1_0;

Tuple_1<int> tuple_1_1;
Tuple_1<int, float, long long> tuple_1_3;

 

 

- 가변인자들의 개수

 

가변인자들의 개수는 sizeof...()함수를 통해서 알 수 있다.

 

다음 예시는 가변인자를 받아서 float형으로 평균을 반환하는 함수이다. sizeof...가 활용된다.

float Sum_f(void)
{
    return 0.0f;
}

template<typename T, typename... Ts>
float Sum_f(T&& arg, Ts&&... args)
{
    return arg + Sum_f(std::forward<Ts>(args)...);
}

template<typename... Ts>
float Average_f(Ts&&... args)
{
    return Sum_f(std::forward<Ts>(args)...) / sizeof...(Ts);
}

 

 

- 가변인자들의 활용

 

가변인자 자체는 함수나 클래스의 구현에 쉽게 사용할 수 없다. 그래서 템플릿을 재귀호출을 통해 단계적으로 분할하면서 활용하는 경우가 있다. 이 때문에 가변인자 템플릿은 재귀로 구현되어 있는 경우를 볼 수 있다. 이를 C++17부터는 fold expression이라는 형식으로 매우 간단하게 표현할 수 있도록 지원하고 있다.

 

 

@ 재귀 호출

바로 위에 나온 코드도 재귀 호출을 통해 구현되었다.

간단히 받은 인자들을 콘솔창에 출력하는 가변인자 템플릿의 예시

void print(void)
{
    cout << " :: print empty\n";
}

template <typename T, typename... Ts>
void print(T&& arg, Ts&&... args)
{
    cout << arg << " / ";
    print(std::forward<Ts>(args)...);
}

int main(void)
{
    print(36.2f, 3000'000'000, 'a', 21000,"test1","test2");
	return 0;
}
36.2 / 3000000000 / a / 21000 / test1 / test2 /  :: print empty

간단히 설명을 하면 print에 여러 인수를 넘겨주게 되면 컴파일러는 해당 인수를 받을 수 있는 함수를 찾아본다. 결국 가변인자 템플릿의 print함수가 선택되고 다시 비슷한 방식으로 재귀적으로 가변인자 템플릿의 함수가 호출된다. 최종적으로 인자가 없는 버전인 print()가 호출되면서 재귀를 탈출하게 된다.

 

 

@ fold expression

fold expression가 어떻게 평가되는지 표로 나타내면 다음과 같다.

fold 형식 평가
Unary right fold (E op ...) (E1 op (... op (EN-1 op EN)))
Unary left fold (... op E)  (((E1 op E2) op ...) op EN)
Binary right fold (E op ... op I) (E1 op (... op (EN−1 op (EN op I))))
Binary left fold (I op ... op E) ((((I op E1) op E2) op ...) op EN)

 

단항 왼쪽 예시

template<typename... Args>
bool all(Args... args) { return (... && args); }
 
bool b = all(true, true, true, false);

//==============================================

template<typename ...Args>
void printer(Args&&... args)
{
    (std::cout << ... << args) << '\n';
}

printer(1, 2, 3, "abc");

 

 

- 가변인자 템플릿 예시

 

https://create-new-worlds.tistory.com/303

 

가변인자 템플릿(Variadic Template) 예시

* printf 일반 가변인자 함수와 비교했을 때 가변인자 템플릿은 타입 안정성을 갖출 수 있다. 일반적으로 우리가 사용하는 printf의 경우 %d와 같이 어떤 형식으로 출력해야할 지 정해줘야 한다. 하

create-new-worlds.tistory.com

 

* 정의

 

템플릿(template)는 주형, 본뜨는 공구라는 뜻을 가지고 있다.

C++에서는 이와 비슷하게 원하는 타입에 따라서 그 타입에 맞게 코드를 생성시켜주는 도구를 의미한다.

데이터 타입을 매개변수로 전달하면 서로 다른 데이터 유형에 대해 동일한 코드를 작성할 필요가 없어진다.

 

 

* 사용 이유

 

함수나 클래스를 처음부터 다시 작성하지 않고 다른 데이터 유형에 따라 생성되도록 해주기 때문에 프로그래밍이 편리하다. 예를 들어 특정 타입의 데이터를 정렬하는 sort라는 함수가 있다고 가정했을 때 각 타입에 대해 여러 함수를 오버로딩하는 것보다 매개변수를 템플릿 타입으로 결정하게 되면 훨씬 관리하기 편하고 오류를 줄일 수 있다.

 

방금 제시한 예시에서와 같이 템플릿은 주로 일반화 프로그래밍(generic programming)에 사용된다. 간단히 일반화된 클래스나 함수를 작성하기 위한 방법으로 사용된다고 할 수 있다.

 

일반화 프로그래밍에 대한 정의는 다음과 같이 할 수 있다.

  • 일반화 유형이 다양한 데이터 타입에 대해 작동하는 알고리즘의 매개 변수로 사용되는 프로그래밍 접근 방식이다.
  • 데이터 형식에 의존하지 않고 하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그래밍 방식이다.

 

하지만 템플릿의 단점도 존재하는데 단점을 고려해서 상황에 적절한 판단 하에 템플릿을 사용해야 한다. 템플릿의 단점은 다음과 같다.

  • 많은 컴파일러는 템플릿 중첩을 지원하지 않는다.
  • 템플릿을 사용하면 모든 코드가 추상화되지 않고 노출된다.
  • 일부 컴파일러는 템플릿에 대한 지원을 많이 하지 않는다.
  • 템플릿 코드에서 오류가 감지되면 판단하기 어려운 오류 메시지가 표시된다.
  • 템플릿으로 개발하는 것이 어려울 수 있다.

 

* 동작 방식

 

- 기본 동작 방식

 

템플릿은 컴파일 타임에 확장(결정)된다는 점에서 템플릿과 공통점을 지닌다. 하지만 매크로와는 다르게 컴파일러가 템플릿을 확장(결정)하기 컴파일러가 타입에 대한 체크를 한다는 점이다. 

 

처음 소스 코드에는 함수/클래스 템플릿만 포함하고 있지만 컴파일된 코드에서는 여러 가지 같은 함수/클래스들로 확장된다.

 

- 종류

 

템플릿은 다음과 같이 크게 두 타입으로 나타난다.

  • 함수 템플릿
  • 클래스 템플릿

 

 

 

- 함수 템플릿

 

함수 템플릿은 단일 함수에 대한 템플릿을 의미하며 이는 여러 데이터 타입과 함께 동작한다. 예를 들어 sort, max, min과 같은 함수 템플릿이 여기에 속한다.

template <typename T> T FunctionName(T a, T b)
{
    // ...
}

보통 다음과 같이 정의된다. T를 함수의 반환형이나 매개변수, 함수바디 등에서 활용할 수 있다. type에는 함수에서 사용 될 데이터 타입이 들어간다.

 

 

- 클래스 템플릿

 

클래스 템플릿도 함수 템플릿과 비슷하게 동작한다. 클래스 템플릿은 클래스가 데이터 타입에 관계 없는 것들을 정의할 때 유용하다.

 

예를 들어 컨테이너 클래스의 경우 내부 동작방식은 동일하지만 안에 들어가는 개체의 타입만 다르기 때문에 클래스 템플릿을 활용하면 매우 효율적이다.

template <typename T> 
class Array 
{
public:
	Array(T arr[], int s)
	{
		ptr = new T[s];
		size = s;
		for (int i = 0; i < size; i++)
		{
			ptr[i] = arr[i];
		}
	}

	void print()
	{
		for (int i = 0; i < size; i++)
		{
			cout << " " << *(ptr + i);
		}
		cout << endl;
	}

private:
	T* ptr;
	int size;
};

 

 

- 템플릿 특수화(Template Specialization)

 

템플릿 특수화는 특수한 경우에 대해서 코드를 다르게 작성하여 다른 동작을 수행할 수 있게 만들어주는 기능이다. 특정 유형에 대해서 다른 동작 방식을 정의함으로써 템플릿을 통해 수행될 수 있게 지원할 수 있다. 특수화 버전은 해당 타입의 함수를 호출했을 때 일반적인 템플릿 정의보다 우선 시되어 먼저 체크하게 된다.

//A generic sort function
template<typename T>
{
    //code to implement quick sort
}

//Template specilization:A function
//specialized for char data type
template<>
void sort<char>(char arr[],int size)
{
    //code to impletement counting sort
}

 

 

* 함수 오버로딩 vs 함수 템플릿

 

함수 오버로딩 함수 템플릿
@ 객체 지향 프로그래밍에서 다형성 특성에 속한다.
@ 여러 함수가 비슷한 동작을 수행할 때 사용된다.

@ 함수 오버로딩은 다양한 수의 인수를 취할 수 있다.
@ 여러 함수가 동일한 작업을 수행하지만 데이터 타입만 다르게 정의해야할 때 사용된다.

@ 템플릿은 다양한 인수를 취하지 못한다.

 

'C++' 카테고리의 다른 글

가변인자 템플릿(Variadic Template) 예시  (0) 2022.05.23
가변인자 템플릿(Variadic Template)  (0) 2022.05.23
람다(Lambda)  (0) 2022.04.11
완벽 전달(Perfect forwarding)의 실패  (0) 2022.03.31
참조 축약  (0) 2022.03.31

* 개인 지식 기반과 여러 곳에서 읽은 것들을 기반으로 작성하였습니다. 틀린 부분이 많을 수 있습니다. 틀린 부분이 있으면 알려주시면 감사하겠습니다.

 

* 상속 기반 설계와 컴포넌트 기반 설계

 

언리얼 엔진에서는 객체 지향형으로 설계되어 있고(상속 기반), 유니티에서는 컴포넌트 기반으로 설계되어 있다.

 

 

- 클래스 예시

 

예를 언리얼 엔진에서는 Pawn를 컨트롤할 수 있는 Actor 종류, Character을 인간형 Pawn으로 계층적으로 정의하고 있다. 하지만 유니티에서는 GameObject라는 Entity(기본 빈 객체)만 있고 그 아래에 컴포넌트를 붙여나가는 식으로 프로그래밍하도록 되어있다.

 

 

- 프로그래밍 예시

 

어떤 액션 게임에서 검, 활, 총 등과 같은 여러 가지 무기를 설계하는 과정을 생각해보자.

 

언리얼 엔진에서는 어떤 클래스를 만들 때 어떤 클래스를 상속 받아서 작성할지 자세히 지정할 수 있게 되어있다. 따라서 Actor를 상속하여 원거리 무기 클래스와 근거리 무기 클래스로 나누고 검은 근거리 무기 클래스를 상속받고, 활과 총은 원거리 무기를 상속받아 작성할 수 있을 것이다. 잘 짜여진 계층구조를 설계해야한다.

 

유니티에서는 클래스를 만들 때 기본적으로 MonoBehaviour라는 컴포넌트를 상속받은 클래스가 생성된다. 무기를 구성하는데 필요한 여러 컴포넌트를 설계한다. 공격력 컴포넌트, 공격범위 컴포넌트, 발사체 컴포넌트 등의 컴포넌트를 설계하고 무기의 특징에 따라 컴포넌트를 조합하여 구성한다.

 

 

- 장단점 비교

 

  객체 지향형 설계 컴포넌트 기반 설계
장점 @ 컴포넌트와 달리 하나로 되어있기 때문에 그 안에서 모두 처리할 수 있다.

@ 계층적 구조를 잘 나타낼 수 있기 때문에 구조를 쉽게 파악할 수 있다.

@ 코드 수정이 쉽다. // 위의 예시에서 발사체에 대한 처리를 고치려면 발사체 컴포넌트 하나를 수정하면 된다.

@ 코드 유지 보수가 쉽고, 확장이 유연하다.
단점 @ 관리해야할 클래스가 많아질수록 계층구조가 더욱 복잡해지고 이를 유지 관리하는데 어려움을 겪거나 시간이 오래걸릴 수 있다.

@ 코드 수정, 추가 과정이 복잡할 수 있다. // 위의 예시에서 발사체에 대한 처리를 고치려면 계층 구조를 확인하고 적절한 곳에 수정해야 한다. 계층 구조를 다시 설계해야할 수도 있다.
@ 여러 컴포넌트가 복합적으로 사용되어야 하는 구조에서 컴포넌트 간 복잡한 상호작용이 필요하다.

@ 계층적 구조를 나타낼 수 없기 때문에 시스템을 세부적인 수준에서 설명하기 어렵다.

 

 

https://spin.atomicobject.com/2020/09/05/unity-component-based-design/?nowprocket=1 

 

Why You Should Use Component-Based Design in Unity

Component-based design in Unity can save developers a lot of time by making the project easier to maintain than the traditional inheritance model.

spin.atomicobject.com

 

https://www.cs.cmu.edu/afs/cs/project/able/ftp/acme-fcbs/acme-fcbs.pdf

 

 

 

 

컴포넌트 설계의 단점

 

First, they provide only a single form of primitive interconnection - method invocation. 
This makes it difficult to represent richer types of component interaction as first class design elements.

Second, they have weak support for hierarchical description, making it difficult to describe systems at increasing levels of detail. 

Third, they do not support the definition of families of systems. 
While they can be used to describe patterns and to define a vocabulary of object types,  they don't have explicit syntactic support for characterizing a class of system in terms of the design constraints that each member of the family must observe. 

Fourth, they do not provide direct support for characterizing and analyzing non-functional properties. This makes it difficult to reason about critical system design properties, such as system performance and reliability

 

 

'게임 엔진 > 비교' 카테고리의 다른 글

[Unity / UnrealEngine] 간단 비교  (0) 2022.04.03

* 정의

 

람다 표현식(lambda expression)(람다(lambda))는 호출되거나 함수에 대한 인수로 전달되는 위치에서 익명 함수 객체를 정의하는 편리한 방법이다.

 

 

* 사용 이유

 

알고리즘 또는 비동기 함수들에 전달되는 몇 줄의 코드를 캡슐화하는 데 사용된다.

 

다음과 같은 곳에서 유용하게 사용된다.

  • _if 알고리즘 : std::find_if, std::remove_if, std::count_if
  • 비교 함수로 커스텀화할 수 있는 알고리즘 : std::sort, std::nth_element, std::lower_bound
  • std::unique_ptr, std::shared_ptr를 위한 삭제자
  • 콜백 함수
  • 인터페이스 적응 함수
  • 일회성 호출을 위한 문맥 국한적 함수

 

 

* 동작 방식

 

- 용어 정리

 

* 람다 표현식(lambda expression)

 : 하나의 표현식으로 소스 코드의 일부이다.

 

* 클로저(closure)

 : 람다에 의해 만들어진 실행시점 객체이다. 캡처 모드(capture mode)에 따라 클로저가 갈무리된 자료의 복사본을 가질 수도 있고 그 자료에 대한 참조를 가질 수도 있다.

 

* 클로저 클래스

 : 클로저를 만드는 데 쓰인 클래스를 말한다. 각각의 람다에 대해 컴파일러는 고유한 클로저 클래스를 만들어 낸다. 람다 안의 문장들은 해당 클로저 클래스의 멤버 함수들 안의 실행 가능한 명령들이 된다.

 

void Lambda_Test(void)
{
	int x = 3;

	// c1은 람다에 의해 만들어진 클로저의 복사본
	auto c1 = [x](int y) { return x * y; };

	// c2는 c1의 복사본
	auto c2 = c1;
}

 

 

- 간단한 동작 방식

 

람다 표현식은 컴파일러가 하나의 클래스(클로저 클래스)를 자동으로 작성해서 그 클래스의 객체(클로저)를 생성하게 만든다.

 

 

* 기본 캡처 모드(default capture mode)

 

C++11의 기본 캡처 모드(default capture mode)는 다음과 같이 두 가지가 있다.

  • 참조에 의한(by-reference) 캡처 모드
  • 값에 의한(by-value) 캡처 모드

기본 참조 캡처 모드에서는 참조가 대상을 잃는 문제가 있을 수 있고, 기본 값 캡처 모드에서는 자기 완결적(self-contained)이지 않을 수 있다. 

 

 

- 기본 참조 캡처 주의 사항

 

참조 캡처를 사용하는 클로저는 지역 변수 또는 람다가 정의된 범위에서 볼 수 있는 매개변수에 대한 참조를 가지게 된다. 하지만 다음과 같은 코드에서는 참조가 대상을 잃는 문제가 발생한다.

std::vector<std::function<bool(int)>> Filters;

void AddDivisorFilter(void)
{
	// ...
	int divisor = Compute();

	// 참조 캡처를 사용하는 과정에서 divisor에 대한 참조가 대상을 잃는다.
	//Filters.emplace_back([&](int value) { return (value % divisor) == 0; });
	Filters.emplace_back([&divisor](int value) { return (value % divisor) == 0; });
}

void Lambda_CaptureMode(void)
{
	std::vector<std::function<bool(int)>> filters;

	AddDivisorFilter(filters);
	
	// 추가 하는 과정에서의 참조 대상 divisor는 지역변수이기 때문에 여기서는 유효하지 않다.
	filters[0](2);
}

람다에 의해 생성된 클러저의 수명이 그 지역 변수나 매개변수의 수명보다 길기 때문에 클로저 안의 참조는 대상을 잃는 현상이 발생한 것이다. [&], [&divisor] 어떤 형식을 사용하여도 마찬가지이다. 하지만 [&divisor]과 같이 명시적으로 써주면 람다의 클로저의 유효성을 좀 더 직관적으로 판단할 수 있다. (divisor가 람다의 클로저 이상으로 오래 살아있어야 한다고 직관적으로 알 수 있다.)

 

 

- 기본 값 캡처 주의 사항

 

위의 참조 캡처의 문제점을 해결하기 위해 다음과 같이 기본 값 캡처 모드를 사용할 수 있다.

void AddDivisorFilter(void)
{
	// ...
	int divisor = Compute();

	Filters.emplace_back([=](int value) { return (value % divisor) == 0; });
}

 

 

@ 포인터에 대한 값 캡처

 

기본 값 캡처 방식은 완전한 해결책은 되지 못한다. 예를 들어 포인터를 값으로 캡처하면 포인터가 람다에 의해 생성된 클로저 안으로 복사되는데, 람다 바깥에서 그 포인터가 삭제될 수 있기 때문이다. 

 

또한 포인터의 값이 복사되었다는 것을 인지하기 어려운 경우도 존재한다. 예를 들어 클래스에서 람다를 사용하는 경우에 그렇다.

class Widget
{
public:
	void addFilter(void) const
	{
		Filters.emplace_back([=](int value) { return (value % _divisor) == 0; });
	}

public:
	int _divisor;
};

얼핏보면 _divisor의 값을 복사해서 사용하는 것처럼 생각할 수 있다. 하지만 캡처는 오직 람다가 생성된 범위 안에서 보이는 static이 아닌 지역 변수(매개변수 포함)에만 적용된다.

 

위의 코드에서 지역변수도 매개변수도 아닌 _divisor에 접근했는데 컴파일 되는 이유는 클래스 내부에서 암묵적으로 this라는 포인터를 사용하고 있기 때문이다. 위의 코드를 명시적으로 표현하면 다음과 같이 표현된다.

void addFilter(void) const
{
    auto currentObjectPtr = this;
    Filters.emplace_back([currentObjectPtr](int value) { return (value % currentObjectPtr->_divisor) == 0; });
}

만약 this를 가리키는 Widget 객체가 소멸한후 람다를 사용한다면 미정의 행동을 유발하게 된다.

 

이를 해결하기 위해서는 지역 변수에 멤버 변수의 값을 복사하고 지역 변수를 값으로 전달하면 된다. 또한 일반화된 람다 갈무리를 사용해서도 해결할 수 있다.

// 지역 변수에 복사한 후 값 캡처
void addFilter(void) const
{
	auto divisorCopy = _divisor;
	Filters.emplace_back([divisorCopy](int value) { return (value % divisorCopy) == 0; });
}

// 일반화된 람다 캡처
void addFilter(void) const
{
	Filters.emplace_back([divisor = _divisor](int value) { return (value % divisor) == 0; });
}

 

 

@ 자기 완결적으로 보이지만 그렇지 않을 수 있다.

 

void AddDivisorFilter(void)
{
	// ...
	static int divisor = Compute();

	Filters.emplace_back([=](int value) { std::cout << divisor << '\n'; return (value % divisor) == 0; });
	++divisor;
}

다음과 같은 코드를 보면 [=]에서 복사된다고 생각할 수 있다. 하지만 이 람다는 어떤 비정적 지역 변수도 사용하지 않기 때문에 캡처 할 수 없다. 캡처는 오직 람다가 생성된 범위 안에서 보이는 static이 아닌 지역 변수(매개변수 포함)에만 적용된다.

 

즉 람다의 코드는 static 변수 divisor을 가리키게 되고 해당 람다를 필터에 추가한 시점에서의 divisor값이 아닌 사용 시점의 divisor값을 사용하게 된다.

 

 

* 초기화 캡처(init capture)(일반화된 람다 캡처(generalized lambda capture))

 

이동 전용 객체(std::unique_ptr, std::future 등)의 경우에는 클로저 안으로 들어가야 하고, 복사는 비싸지만 이동은 저렴한 객체들 역시 클로저 안으로 들여오는 것이 좋다. C++14부터 객체를 클로저 안으로 이동하는 수단을 직접 제공한다.

 

이를 실현시키는 수단은 초기화 캡처(init capture)라고 불리는 방법이다. 이 방법은 C++11의 캡처에서 할 수 있는 모든 것들을 할 수 있으며 그 외 여러 가지 기능도 지원한다.

 

 

- 초기화 캡처(C++14)

 

초기화 캡처로 다음과 같은 것들을 지정할 수 있다.

  • 람다로부터 생성되는 클로저 클래스에 속한 자료 멤버의 이름
  • 그 자료 멤버를 초기화하는 표현식
void Lambda_InitCapture(void)
{
	auto pw = std::make_unique<Widget>();

	auto func = [pw = std::move(pw)]{ return pw->isValidated(); };
}

위와 같은 코드에서 좌변 pw는 클로저 클래스 안의 자료 멤버를 지칭하고 우변 pw은 람다 이전에 선언된 객체를 뜻한다. 이를 종합해보면 다음과 같다.

클로저 안에서 자료 멤버 pw를 생성하되, 지역 변수 pw에 std::move를 적용한 결과로 그 자료 멤버를 초기화 하라.

람다 본문의 코드는 클로저 클래스의 범위 안에 있으므로 본문에 있는 pw는 클로저 클래스의 해당 자료 멤버를 지칭한다.

 

이처럼 C++14에서는 어떤 표현식의 결과를 캡처할 수 있도록 지원하고 있는 것이다. 이를 일반화된 람다 캡처(generalized lambda capture)라고 부르기도 한다.

 

위의 람다식에 의해 생성되는 클래스는 다음과 비슷하게 만들어진다.

class ClosureClass
{
public:
	explicit ClosureClass(std::unique_ptr<Widget>&& ptr)
		: pw(std::move(ptr)) {}

	bool operator()() const
	{
		return pw->isValidated();
	}

private:
	std::unique_ptr<Widget> pw;
};

 

 

- 초기화 캡처 흉내(C++11)

 

C++11에서 이를 흉내내기 위해서는 다음과 같은 방식을 사용해야한다.

  • 캡처할 객체를 std::bind가 산출하는 함수 객체로 이동시킨다.
  • 캡처 객체에 대한 참조를 람다에 넘겨준다.

 

C++14와 비교해서 보면 다음과 같다.

void Lambda_InitCaptureCopy(void)
{
	std::vector<double> data;
	// ...
	
	// C++14
	auto func1 = [data = std::move(data)]{ /* data[index] ... */ };

	// C++11
	auto func2 = std::bind([](const std::vector<double>& data) { /* data[index] ... */ },
						   std::move(data));
}

 

위의 상황을 그림으로 다음과 같이 나타낼 수 있다.

 

여기서 다음과 같은 사실을 확인할 수 있다.

 

  • std::bind는 호출 가능한 함수 객체(바인드 객체)를 돌려준다.
  • std::bind의 첫 인수는 호출 가능한 객체이고, 나머지 인수는 그 객체에 전달할 값들을 나타낸다.
  • std::bind에 전달된 모든 인수의 복사본들을 포함한다.
  • 람다가 생성한 클로저 또한 복사되어 바인드 객체에 저장된다.
  • C++11에서는 C++14에서 처럼 클로저 안으로 객체를 이동 생성하는 것이 불가능하기 때문에 바인드 객체에 저장한다.
  • 바인드 객체에 저장된 객체는 클로저에 참조로 전달한다.
  • 복사된 클로저와 바인드 객체의 수명은 동일하기 때문에 바인드 객체 안의 객체들을 마치 클로저 안에 있는 것처럼 취급할 수 있다. 
  • std::bind는 전달된 인수에 대하여 왼값은 복사 생성된 객체, 오른값은 이동 생성된 객체를 가지고 있다.
  • 람다로부터 만들어진 클로저 클래스의 operator(), 모든 자료 멤버는 const이다.
  • 바인드 객체 안의 이동 생성된 data 복사본은 const가 아니다. 만약 data를 변경해야한다면 클로저의 operator함수를 non-const함수로 변경할 필요가 있다. 이는 람다를 작성할 때 mutable키워드를 붙여서 작성함으로써 실현된다.

 

 

* 람다의 매개변수와 auto : 일반적 람다(generic lambda)

 

람다 표현식의 매개변수에도 auto 키워드를 사용할 수 있다. 이를 일반적 람다(generic lambda)라고 한다.

일반적인 auto를 사용한 람다 표현식은 다음과 같다.

auto f1 = [](auto x) { return normalize(x); };

 

 

만약 완벽 전달을 수행하려면 보편 참조 형식으로 바꿔주면 된다. 다만 매개변수를 다른 곳으로 넘겨줄 때 템플릿에서의 보편참조와 같이 std::forward<T>를 사용하지 못한다. (T라는 것을 알지 못하기 때문에)

 

하지만 그냥 자료형 그대로 넘겨주어도 문제가 없다. 참조 축약 규칙에 의해 오른값(int&& 등)을 넘겨주어도 오른값(int&& 등)을 돌려받는다.

auto f2 = [](auto&& x) { return normalize(std::forward<decltype(x)>(x)); };

 

 

여기서 많은 가변 매개변수를 받고 싶다면 다음과 같이 수정하면 된다.

auto f3 = [](auto&&... xs) { return normalize(std::forward<decltype(xs)>(xs)...); };

 

 

* 람다 vs std::bind

 

결론부터 말하자면 람다가 가독성, 편의성 등 모든 면에서 뛰어나다고 볼 수 있다.

 

예시로 어떤 지점에서 한 시간 후부터 30초간 소리를 내는 함수가 있다고 가정한다.

enum class Sound { Beep, Siren, Whistle };
using Time = std::chrono::steady_clock::time_point;
using Duration = std::chrono::steady_clock::duration;


void SetAlarm(Time t, Sound s, Duration d) { /* ... */ }

void Lambda_Bind(void)
{
	auto setSoundByLambda = [](Sound s)
	{
		using namespace std::chrono;
		using namespace std::literals;

		SetAlarm(steady_clock::now() + 1h,
				 s,
				 30s);
	};

	using namespace std::chrono;
	using namespace std::literals;
	using namespace std::placeholders;

	auto setSoundByBind =
		std::bind(SetAlarm,
				  steady_clock::now() + 1h,
				  _1,
				  30s);
}

 

람다와 바인드를 사용해 구현하면 위와 같이 구현된다.

 

- 람다는 바인드에 비해 명확하고 가독성이 좋다.

 

바인드는 자리표(placeholder)라는 것을 통해 setSoundByBind의 첫인수를 SetAlarm의 두번째 인수로 전달하는 역할을 한다. 바인드 객체를 통해 인수가 어떤 SetAlarm의 어떤 매개변수로 들어가는지 신경써야한다. 람다는 간단히 봐도 정확하게 파악할 수 있다.

 

한가지 예를 들어보자면 다음과 같다.

void Lambda_Bind(void)
{
	int lowVal = 3;
	int highVal = 10;

	auto betweenByLambda = [lowVal, highVal](const auto& val)
	{ return lowVal <= val && val <= highVal; };

	using namespace std::placeholders;
	auto betweenByBind =
		std::bind(std::logical_and<bool>(),
				  std::bind(std::less_equal<int>(), lowVal, _1),
				  std::bind(std::less_equal<int>(), _1, highVal));

}

 

 

- 바인드 사용 시, 바인드 시점에 값이 결정되지 않도록 지연시킬 필요가 있다.

 

위의 코드에서 람다는 setSoundByLambda를 호출할 때 시간이 계산되어 한 시간 후 부터 30초간 알람이 울린다. 하지만 바인드의 경우 std::bind를 호출할 때 시간이 계산되어 이 시점기준으로 한 시간 후 30초간 알람이 울리게 된다. 이를 방지하기 위해서 다음과 같은 코드를 작성해야한다. 

	auto setSoundByBind =
		std::bind(SetAlarm,
				  std::bind(std::plus</*steady_clock::time_point*/>(),
							std::bind(steady_clock::now),
							1h),
				  _1,
				  30s);

 

 

- 바인드 사용 시, 함수의 이름 밖에 알지 못하기 때문에 중복적재된 함수를 적절하게 선택할 수 없다.

 

이를 명시적으로 std::bind에 알려줘야 한다.

void SetAlarm(Time t, Sound s, Duration d) { /* ... */ }
// 중복적재 추가
void SetAlarm(Time t, Sound s, Duration d, int factor) { /* ... */ }

void Lambda_Bind(void)
{
	// ...
	using namespace std::chrono;
	using namespace std::literals;
	using namespace std::placeholders;
	using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
    
	auto setSoundByBind =
		std::bind(static_cast<SetAlarm3ParamType>(SetAlarm),
				  std::bind(std::plus</*steady_clock::time_point*/>(),
							std::bind(steady_clock::now),
							1h),
				  _1,
				  30s);
}

 

 

- 바인드 사용 시, 함수 포인터를 통해 호출이 일어나기 때문에 컴파일러가 인라인화할 가능성이 더 낮다.

 

- 바인드 사용 시, 매개변수의 저장 방식(std::bind에서 매개변수로 받은 값들을 저장할 때)에 대해 알아야한다.(값으로 전달된다.) 람다에서는 캡처 방식에 따라서 명확하게 확인할 수 있다.

 

- 바인드 사용 시, 바인드 객체를 호출 시 인수의 전달 방식을 알야아 한다.(참조로 전달된다.)

 

 

바인드는 C++11에서만 가끔 쓰이는 경우가 있는데(C++14에서는 쓰이지 않는다.) 쓰이는 경우는 다음 두 가지이다.

  1. 이동 캡처 구현
  2. 다형적 함수 객체(다양한 인수들을 받을 수 있게 하기 위해)

 

'C++' 카테고리의 다른 글

가변인자 템플릿(Variadic Template)  (0) 2022.05.23
템플릿(Template)  (0) 2022.05.23
완벽 전달(Perfect forwarding)의 실패  (0) 2022.03.31
참조 축약  (0) 2022.03.31
보편 참조에 대한 중복적재 대안  (0) 2022.03.31

* 전반적인 용어 비교

 

Category Unity UE4
Gameplay Types Component Component
  GameObject Actor, Pawn
  Prefab Blueprint Class
Editor UI Hierarchy Panel World Outliner
  Inspector Details Panel
  Project Browser Content Browser
  Scene View Viewport
Meshes Mesh Static Mesh
  Skinned Mesh Skeletal Mesh
Materials Shader Material, Material Editor
  Material Material Instance
Effects Particle Effect Effect, Particle, Cascade
  Shuriken Cascade
Game UI UI UMG (Unreal Motion Graphics)
Animation Animation Skeletal Animation System
  Mecanim Persona , Animation Blueprint
2D Sprite Editor Paper2D
Programming C# C++
  Script Blueprint
Physics Raycast Line Trace, Shape Trace
  Rigid Body Collision, Physics
Runtime Platforms iOS Player, Web Player Platforms

 

 

* 프로그래밍

 

 

 

* 월드 표현

 

  UnrealEngine Unity
표현 방식 Actor는 RootComponent를 가지고 있으며 이는 SceneComponent의 서브클래스이다. SceneComponent는 계층적인 구조를 이루며 이에 따라 계층적으로 적용되는 위치, 회전, 스케일 정보를 가지고 있다. GameObject는 Transform 컴포넌트를 가지고 있다. 이 Transform는 계층적인 구조를 이룰 수 있으며 위치, 회전, 스케일 정보를 계층적으로 나타낸다.

 

 

 

 

* 객체 생성

 

- 유형별 생성 방법

 

 객체  유형 UnrealEngine Unity 역할
Actor
GameObject
Actor : World 객체를 찾고(일부 객체가 World 객체를 가져올 수 있도록 지원), World 객체의 SpawnActor로 생성하고자 하는 Actor의 클래스를 넘겨서 생성 GameObject : Instantiate함수를 통해 생성 월드에 생성되는 객체들로 대부분의 게임 로직을 담당한다.
UObject
ScriptableObject
UObject : NewObject를 통해 생성 ScriptableObject : ScriptableObject.CreateInstance함수를 통해 생성 월드에 스폰할 필요없거나 Actor처럼 컴포넌트를 포함하는 게임플레이 관련 클래스에 유용하다.

 

// Unity의 GameObject
GameObject NewGO = (GameObject)Instantiate(EnemyPrefab, SpawnPosition, SpawnRotation);
NewGO.name = "MyNewGameObject";

// UE의 Actor
UWorld* World = ExistingActor->GetWorld();
FActorSpawnParameters SpawnParams;
SpawnParams.Template = ExistingActor;
World->SpawnActor<AMyActor>(ExistingActor->GetClass(), SpawnLocation, SpawnRotation, SpawnParams);

 

// Unity의 ScriptableObject
MyScriptableObject NewSO = ScriptableObject.CreateInstance<MyScriptableObject>();

// UE의 UObject
UMyObject* NewObj = NewObject<UMyObject>();

 

 

- 생성 과정

 

Unity UnrealEngine
디폴트 값을 설정하기 위해 선언과 동시에 초기화한다. 각 객체 클래스에는 디폴트 값의 속성들과 컴포넌트들을 클래스 기본 객체(CDO)를 포함하고 있다. 이는 엔진 초기화 시 생성자를 통해서 최초로 생성되며 수정되지 않은 상태로 유지된다.

해당 객체를 생성할때 CDO에서 복사해서 생성한다.

 

Unity / UnrealEngine

 

 

* 캐스팅

 

 

 

* 트리거

 

Unity

public class MyComponent : MonoBehaviour
{
    void Start()
    {
        collider.isTrigger = true;
    }
    void OnTriggerEnter(Collider Other)
    {
        // ...
    }
    void OnTriggerExit(Collider Other)
    {
        // ...
    }
}

 

UnrealEngine

UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

    // My trigger component
    UPROPERTY()
    UPrimitiveComponent* Trigger;

    AMyActor()
    {
        Trigger = CreateDefaultSubobject<USphereComponent>(TEXT("TriggerCollider"));

        // Both colliders need to have this set to true for events to fire
        Trigger.bGenerateOverlapEvents = true;

        // Set the collision mode for the collider
        // This mode will only enable the collider for raycasts, sweeps, and overlaps
        Trigger.SetCollisionEnabled(ECollisionEnabled::QueryOnly);
    }

    virtual void NotifyActorBeginOverlap(AActor* Other) override;

    virtual void NotifyActorEndOverlap(AActor* Other) override;
};

 

 

* 입력

 

Unity

public class MyPlayerController : MonoBehaviour
{
    void Update()
    {
        if (Input.GetButtonDown("Fire"))
        {
            // ...
        }
        float Horiz = Input.GetAxis("Horizontal");
        float Vert = Input.GetAxis("Vertical");
        // ...
    }
}

 

UnrealEngine

UCLASS()
class AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

    void SetupInputComponent()
    {
        Super::SetupInputComponent();

        InputComponent->BindAction("Fire", IE_Pressed, this, &AMyPlayerController::HandleFireInputEvent);
        InputComponent->BindAxis("Horizontal", this, &AMyPlayerController::HandleHorizontalAxisInputEvent);
        InputComponent->BindAxis("Vertical", this, &AMyPlayerController::HandleVerticalAxisInputEvent);
    }

    void HandleFireInputEvent();
    void HandleHorizontalAxisInputEvent(float Value);
    void HandleVerticalAxisInputEvent(float Value);
};

 

 

* 주요 함수

 

- GameObject / Actor 에서 Component 가져오기

 

Unity

MyComponent MyComp = gameObject.GetComponent<MyComponent>();

 

UnrealEngine

UMyComponent* MyComp = MyActor->FindComponentByClass<UMyComponent>();

 

 

- Component 에서 GameObject / Actor 가져오기

 

Unity

MyComponent.gameObject;

 

UnrealEngine

MyComponent->GetOwner();

 

 

- 특정 GameObject / Actor 찾기

 

Unity

// Find GameObject by name
GameObject MyGO = GameObject.Find("MyNamedGameObject");

// Find Objects by type
MyComponent[] Components = Object.FindObjectsOfType(typeof(MyComponent)) as MyComponent[];
foreach (MyComponent Component in Components)
{
        // ...
}

// Find GameObjects by tag
GameObject[] GameObjects = GameObject.FindGameObjectsWithTag("MyTag");
foreach (GameObject GO in GameObjects)
{
        // ...
}

// Find Actor by name (also works on UObjects)
AActor* MyActor = FindObject<AActor>(nullptr, TEXT("MyNamedActor"));

// Find Actors by type (needs a UWorld object)
for (TActorIterator<AMyActor> It(GetWorld()); It; ++It)
{
        AMyActor* MyActor = *It;
        // ...
}

 

UnrealEngine

// Find UObjects by type
for (TObjectIterator<UMyObject> It; It; ++It)
{
    UMyObject* MyObject = *It;
    // ...
}

// Find Actors by tag (also works on ActorComponents, use TObjectIterator instead)
for (TActorIterator<AActor> It(GetWorld()); It; ++It)
{
    AActor* Actor = *It;
    if (Actor->ActorHasTag(FName(TEXT("Mytag"))))
    {
        // ...
    }
}

 

 

+ Recent posts