Post

[ STUDY/Effective C++ ] 2022. 6. 23. 20:41

대입(assignment)과 초기화(initialization)를 헷갈리지않기

class People {...};

class Person {
public:
	Person(const std::string& name, const std::int& age);
    
private:
	std::string theName;
	std::string theAge;
};

Person::Person(const std::string& name, const std::int& age)
{
	theName = name;
	theAge = age;
}

위 코드는 초기화가 아니라 대입이다. 

Person::Person(const std::string& name, const std::int& age)
	: theName(name),
	  theAge(age)
{}

위 코드는 대입이 아닌 초기화이다.

앞의 코드는 초기화를 따로하고, 대입을 따로 하기때문에 처음 초기화한 값은 헛것이 되어버린다.

하지만 멤버 초기화 리스트를 이용하면, 복사 생성자를 통해 들어가는 인자가 바로 데이터 멤버에 대한 생성자의 인자로 쓰인다. 이런 면에서 앞의 코드보다 좀 더 효율적이다.

 

Person::Person(const std::string& name, const std::int& age)
	: theName(), //theName의 기본ctor(생성자)호출
	  theAge()
{}

이렇게 매개변수가 있든없든간에 멤버 초기화 리스트에 모두 넣어주는 쪽이 좋다. 

 

비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.

정적 객체 : 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체 (스택, 힙기반 객체는 애초에 될 수 없음)

                  - 전역 객체

                  - 네임스페이스 유효범위에서 정의된 객체

                  - 클래스 안에서 static으로 선언된 객체

                  - 함수 안에서 static으로 선언된 객체

                  - 파일 유효범위에서 static으로 정의된 객체

 

함수 안에 있는 개체 -> 지역 정적 객체 (함수라는 지역성을 가짐)

나머지 -> 비지역 정적 객체

 

별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 정해져있지않다.

이러한 사실로 인해 초기화되지도 않은 객체를 사용할 일이 생길수도 있다는것이다.

 

이는 비지역 정적 객체를 지역 정적 개체로 바꾸면 해결되는 일이다.< 단일체 패턴 >

비지역 정적 객체를 하나씩 맡는 함수를 준비 - 안에 객체를 넣음 - 함수에서는 그 객체에 대한 참조자를 반환하게함.

=> 사용자가 직접 비지역 정적 객체를 참조x , 함수호출로 대신함! 

Post

[ STUDY/Effective C++ ] 2022. 6. 18. 18:24

const의 장점

- 클래스 바깥에서 전역 혹은 네임스페이스 유표범위의 상수를 선언하는데 쓸 수 있다.

- 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있다.

- 클래스 내부에서 정적 멤버 및 미정적 데이터 멤버 모두를 상수로 선언할 수 있다.

- 포인터 자체를 상수로, 포인터가 가리키는 데이터를 상수로 지정할 수 있다.

 

 

상수 포인터

char greeting[] = "Hello";
char *p = greeting; //비상수 포인터, 비상수 데이터

const char *p = greeting;// 비상수 포인터, 상수 데이터

char * const p = greeting; //상수 포인터, 비상수 데이터

const char* const p = greeting; //상수 포인터, 상수 데이터

const가 *의 왼쪽에 있으면 포인터가 가리키는 대상이 상수,

const가 *의 오른쪽에 있으면 포인터 자체가 상수

 

void f1(const Widget *pw);

void f2(Widget const *pw)

두 가지 형태 모두 함수가 받아들이는 매개변수 타입(상수 Widget 객체에 대한 포인터)이 똑같다.

 

 

반복자

const std::vector<int>::iterator iter = vec.begin();
*iter = 10;
++iter; //에러!

////////////////////////////////////////////////////
const std::vector<int>::const_iterator iter = vec.begin();
*iter = 10; //에러!
++iter;

반복자를 const로 선언하는것은 포인터를 상수로 선언하는것과 같다. 

반복자는 자신이 가리키는 대상이 아닌것을 가리키는 경우가 허용되지 않지만, 반복자가 가리키는 대상 자체는 변경이 가능하다. 만약 불가능한 객체를 가리키는 반복자가 필요하다면 const_iterator를 쓰면 된다.

 

그냥 iterator은 가리키는 대상의 변경이 가능하지만, const_iterator은 변경이 불가능하다.

 iterator은 상수이기때문에 대상의 값을 변경하는게 불가능하지만, const_iterator은 변경이 가능하다.

 

 

함수선언

함수 반환 값, 매개변수, 멤버 함수 앞, 함수 전체 에 대해서 const의 성질을 붙일 수 있다.

함수 반환값을 상수로 정해 주면, 안전성과 효율을 가지고가며 에러 돌발상황을 줄일 수 있다.

 

 

상수 멤버 함수

멤버 함수에 붙는 const키워드의 역할 : 해당 멤버 함수가 상수 객체에 대해 호출될 함수란것을 알려줌

 

이 역할의 중요성

1. 클래스의 인터페이스를 이해하기 좋게 하기위해서

그 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고, 또 변경할 수 없는 함수는 무엇인가를 사용자가 알아야함

 

2. 이 키워드를 통해 상수 객체를 사용할 수 있게 함

상수 상태로 전돨된 객체를 조작할 수 있는 const 멤버 함수(상수 멤버 함수) 가 준비되어야 c++ 프로그램의 실행 성능을 높이는 객체 전달을 상수객체에 대한 참조자로 진행할 수 있다.

 

 

오버로딩

const 키워드의 유무로 멤버함수들의 오버로딩이 가능하다.

 

 

비트수준 상수성(물리적 상수성)과 논리적 상수성

비트수준 상수성은 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버함수가 'const'임을 인정하는 개념이다.

C++에서 정의하고 있는 상수성이 비트수준 상수성이다.그래서 상수 멤버 함수는 그 함수가 호출된 객체의 어떤 비정적 멤버도 수정할 수 없게되어있다.

class CTextBlock{
public:
	...
    char& operatorr[] (std::size_t position) const
    { return pText[position];}
    
private:
	char *pText;
};



const CTextBlock cctb("Hello");
char *pc = &cctb[0];
*pc = 'J'; //cctb는 Jello가 된다.

코드를 보면 알다시피 operator함수가 비트수준 상수성이 있음으로 인해 상수 멤버 함수로 선언되어있다.

(원래라면 틀린거지만, 코드 내에서 pText를 건드리지 않기때문에 허용)

 

하지만, 어떤 값으로 초기화된 상수 객체를 하나 만들어 놓고 여기다 상수 멤버 함수를 호출하였더니 값이 변했다!

(pText를 건들지않아서 컴파일러가 넘어가줬는데!)

 

이를 보완하는것이 바로 논리적 상수성이다.

상수 멤버 함수라고해서 객체의 한 비트도 수정이 불가능한게 아니라, 몇 비트정도는 바꿀 수 있게하는것이다. 

class CTextBlock {
public:
	...
    std::size_t length() const;
    
private:
	char *pText;
    std::size_t textlength;
    bool lenthIsValid;
};

std::size_t CTextBlock::length() const
{
	if( !lengthIsValid) {
    	textLength = std::strlen(pText);
        lengthIsvalid = true;
}

 return textLength;
 }

위 코드에서 상수 멤버 함수 안에서는 textLength, lengthIsValid에 대입을 나타내는 코드인 만큼 비트수준 상수성과는 멀리 떨어져있다. 이 경우 컴파일러에서 오류를 띄우게된다.

 

    mutable std::size_t textlength;
    mutable bool lenthIsValid;

이때, mutable을 쓰게된다면 비정적 데이터 멤버를 비트수준 상수성에서 벗어나게해준다.

 

 

상수멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법

똑같은 역할을 하는 함수가 const의 유무에따라서 한번 더 쓰여 코드 중복이 된다면 컴파일시간, 유지보수, 코드 크기 등의 신경써야할것들이 늘어나게된다. 물론 보기에도 안좋고!

 

class TextBlock {
public:
	...
    const char& operator[] (std::size_t position) const
    {
    ...
    ...
    ...
    return text[position];
    }
    
    char& operator[] (std::size_t position)
    {
     return
     	const_cast<char&>( //캐스팅 적용, const떼어냄
        
         static_cast<const TextBlock&> //*this의 타입에 const붙임,op[]상수 버전 호출
          (*this) [position] 
       );
    }
    ...
};

해결법으로 캐스팅을 쓰며 안성정도 유지하며 코드중복을 피하는 방법은 비상수 operator[]가 상수 버전을 호출하도록 구현하는것이다. 총 두번의 캐스팅을 썻는데, 첫번째 캐스팅은 *this에 const를 붙이기 위함(static_cast)이고 두번째 캐스팅은 상수operator[]의 반환 값에서 const를 떼어내는 캐스팅(const_cast)이다. 

 

 

 

 

 

 

 

 

Post

[ STUDY/Effective C++ ] 2022. 6. 18. 01:48

#define 소스코드의 경우에는 컴파일러에게 넘어가기 전, 선행처리자에서 숫자 상수로 바꾸어버린다.

그래서 #define pie 3.141 이라면

pie라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않고 숫자 상수(3.141)로 대체된다.

 

CONST

하지만 #define대신에 const를 사용해

const double pie = 3.141; 을 작성한다면 상수타입의 데이터이기 때문에 컴파일러에서도 보이게된다. 

 

그리고 매크로인 #define을 쓰게된다면 3.141의 사본이 등장횟수만큼 코드안에 들어가지만, 상수 타입인 const를 쓰면 사본이 한개만 생성되어 최종 코드의 파일 크기가 더작아지게된다. 

 

 

※ #define을 상수로 교체할때의 주의할 점

1. 상수 포인터를 정의하는 경우

포인터는 꼭 const로 선언, 포인터가 가리키는 대상까지 const로 선언

const char * const alphabet = "ABCD";
const std::string alphabet = "ABCD";//위보다 더 좋은방법

 

2. 클래스 멤버로 상수를 정의하는 경우

Class GamePlayer{
private:
	static const int  NumTurns = 5; // 상수선언
	int scores[NumTurns];
	...
};

3번째 줄의 코드는 '정의'가 아닌 선언이다. 정적 멤버로 만들어지는 정수류 타입의 클래스 내부 상수는 정의 없이 선언만 해도 아무 문제없다.

const int GamePlayer::NumTurns; //NumTurns정의

하지만 컴파일러가 정의를 달라고 떼쓰는 경우에는 위와 같은 클래스 상수 정의를 '구현파일'에 둔다.

선언된 시점에서 상수의 초기값이 주어지기때문에, 정의에서는 초기값이 주어지면 안된다.

 

#define으로는 클래스 상수를 정의할 수도, 캡슐화 혜택도 받을 수 없다.

 

class CostEstimate{
private:
	static const double FudgeFactor; //정적 클래스 상수 선언(헤더파일에 두기)
	...
};
const double
	CostEstimate::FudgeFactor = 1.35; //정적 클래스 상수 정의(구현파일에 두기)

오래된 컴파일러의 경우에는 반대로 선언된 시점에서 초기값을 주는게 아닌, 정의 시점에 초기값을 주도록해야한다.

 

 

ENUM

여기서 예외)

Class GamePlayer {
private:
	enum {NumTurns = 5 }; //5에대한 기호식 이름으로 만듬
    
    int scores[NumTurns];
    ...
};

오래된 컴파일러를 배려해서 '나열자 둔갑술'(enum hack) 기법을 활용하자.

나열자 타입의 값은 int가 놓일 곳에도 쓸 수 있다. 원래라면 클래스내 초기화 금지로인해 위의 int scores[NumTurns]같은 배열멤버선언은 오래된 컴파일러에서 동작하지못한다.

 

 

INLINE

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b)) //#define을 쓴 경우


template<typename T> //inline에 대한 Template을 쓴 경우
inline void callWithMax(const T& a, const T& b)
{
	f(a>b ? a:b);
}
CALL_WITH_MAX(++a,b);
CALL_WITH_MAX(++a,b+10);

매크로를 작성할 때는 매크로 본문에 들어 있는 인자마다 반드시 괄호를 씌워주어야한다.

하지만 inline을 쓴다면 그런 괄호들을 굳이 씌울 필요가 없다.

 

이런 눈에 보이는 부분들이 아니더라도

밑의 코드들을 보았을때 #define매크로를 사용한다면 f가 호출되기도 전에 a가 증가하는 횟수가 달라지게 된다. 

하지만 inline을 쓴다면 인자를 여러번 평가할지 모른다는 걱정도 없다.

그리고 매크로가 아닌 진짜 함수이기때문에 함수의 유효범위 및 접근규칙을 그대로 따라가기에 기존 매크로의 효율유지는 와 함께 함수의 모든 동작방식 및 타입의 안정성까지 챙길 수 있다.

 

 

 

 

Post

[ PROJECT/유니티 ] 2022. 6. 17. 16:09

유니티 3D 게임 프로젝트를 진행하게되면서 최적화의 중요성을 많이 알게되었습니다.

빌드자체에서 걸리는 시간이라던가 게임플레이 도중에 발생하는 지연이라던가 그런것들을 보며 구현도 중요하지만 구현한것을 원활하게 돌아갈 수 있도록 최적화하는것이 정말 중요하구나를 깨달으며 프로젝트에 적용한 최적화 방법을 소개할까 합니다.

 

부모-자식 간의 관계를 최소화로 하자

유니티에서 Hierarchy의 depth가 깊어지면 깊어질 수록 부모의 결과를 전파하기 위한 계산이 추가됩니다.

 

부모에 대한 position이나 rotation 등 정보가 변경되면 자식들에게도 메세지를 보내게 되므로 성능에는 악영향을 끼치게됩니다.

 

 

그래서 저는 프로젝트내에서 건설과 관련된 오브젝트를 맡았었는데..

왼쪽이 이전에 있던 오브젝트 하나이고, 오른쪽이 하나의 오브젝트를 다 떼어내어 따로 분류를 해놓은 부모-자식간의 관계를 최소화로 만든 Prefeb입니다. 저는 원래 Building하나로 오브젝트 풀에 넣어 재사용했던것을 모두 다 떼어놓아 오브젝트들 모두 각자의 prefeb으로 해결했습니다.

 

확실히 왼쪽에 있었을때의 코드들도 항상 부모-자식을 참조하고 있었기에 좋은코드라고 볼 수 없었고 저도 만들면서 일단 완성하는거에 초점을 두자라고 생각을 하며 만들긴했어서 많이 더러웠습니다.

 

하지만 최적화에 신경을 쓰게 되면서 제가 만든 오브젝트가 정말 이상한모양이라는것을 깨닫고 오른쪽처럼 기존의 오브젝트들을 하나씩 다 떼어놓고 코드들도 정리하고나니 정말 보기에도 좋고 이해하기에도 좋은 결과물이 나왔습니다.

 

좀 더 확실한 결과를 보여드리기위해 profiler 을 이용해 비포&애프터 사진을 찍어 성능비교를 하고싶었으나, 프로젝트를 하는도중에 이전 결과물이 날아가버려 (ㅠㅜ..) 구체적인 성능을 비교할 순 없었습니다.

 

하지만 계층구조가 깊으면 깊을수록 자식들의 수가 많아져 그만큼 연산(Transform...)이 늘어나기때문에 속도를 위해서라도 부모-자식 간의 관계를 최소화하는것이 좋습니다.

▲ top