No older revisions available
No older revisions available
MoreEffectiveC++
1.1. Item 16:Remember the 80-20 rule. ¶
80-20 규칙 이란? 프로그램의 80%의 리소스가 20%의 코드에서 쓰여진다.:실행 시간의 80%가 대략 20%의 코드를 소모한다;80%의 메모리는 어떤 20%의 코드에서 쓴다.;80%의 disk 접근은 20%의 코드에서 이루어진다.:80%의 소프트웨어 유지의 노력은(maintenance effort)는 20%의 코드에 쏟아 부어진다.
80-20 규칙은 수많은 기계에서, 운영체제(Operating System)에서, 그리고 어플리케이션에서 적용된다. 80-20 규칙은 단지 재미있는 표현보다 더 많은 의미가 있다.;그것은 광범위하고, 실질적인 개념이 필요한 시스템의 성능(능률)에 개선 대한 기준점을 제시한다.
80-20 규칙은 수많은 기계에서, 운영체제(Operating System)에서, 그리고 어플리케이션에서 적용된다. 80-20 규칙은 단지 재미있는 표현보다 더 많은 의미가 있다.;그것은 광범위하고, 실질적인 개념이 필요한 시스템의 성능(능률)에 개선 대한 기준점을 제시한다.
80-20 규칙을 생각할때 이 숫자에 너무 매달릴 필요는 없다. 때로 사람들은 90-10이 될수도 있는거고, 실험적인 근거도 역시나 그렇게 될수 있는 것이다. 정확한 숫자이든, 중요한 사안,포인트는 바로 이것이다.: 당신의 소프트웨어의 수행의 부하는 거의 항상 소프트웨어의 작은 부분에서 결정되어 진다는 점이다.
프로그래머의 노력이 당신의 소프트웨어의 성능 개선에 촛점을 맞추게 된다면 80-20 규칙은 당신의 생활을 간편하게(윤택하게), 혹은 좀더 복잡히(어렵게) 만들어 나갈것이다. 간편하게(윤택하게) 쪽을 생각한다면, 80-20 규칙은 당신이 성능에 대하여 솔직히 어느 정도 평범한 코드의 작성을 대다수에 시간을 보낼수 있음을 의미한다.왜냐하면 당신이 일하는 시간의 80%에 작성된 것은 시스템의 성능에 관해 특별히 해를 끼치지 않는다는 의미이기 때문이다. 저의미는 아마 많은 부분이 당신을 위한 말은 아니지만, 그것은 당신의 스트레스 정도를 다소 줄여줄수 있다. 복잡히(어렵게)를 생각해 본다면 80-20 규칙은 만약 당신이 성능문제를 가지고 있다면 당신 앞에 놓여진 일은 험하다는 걸 의미한다. 왜냐하면, 당신은 오직 그 문제를 일으키는 작은량의 코드들을 제거해야 하고, 성능을 비약적으로 향상시키는 방법을 찾아야 하기 때문이다. 이렇게 80-20 규칙은 두가지의 반대되는 다른 관점에서의 접근이 주어진다.:대다수 사람들은 그렇게하고, 옯은 방법을 행해야 할것이다.
많은 사람들이 병목현상(bottleneck)에 관한 해결책에 고심한다. 경험에 따른 방법, 직관력, tarot 카드이용(운에 맏기기) 그리고 Ouija(점괘를 나타내는 널판지의 상표명, 즉 점보기) 보드를 사용 하기도 하고, 소문이나 잘못, 올바르지 않은 메모리 할당, 충분하지 않은 최적화를 한 컴파일러, 혹은 치명적인 순환 구문을 만들어내기 위해 어셈블리 언어를 사용한 돌대가리 메니저들의 메니저들. 이러한 사정들은 일반적으로 멸시의 비웃음을 동반하고, 그들의 예언은 솔직히 잘못된 것이다.
(DeleteMe 이후 영어 해석이 너무 모호하다. 사실 내용을 잘 이해를 못했다. 차후 고치자. 이 전도 마찬가지)
대부분 프로그래머들은 그들의 프로그램에 관한 특성에 관하여 멍청한 직관력을 가지고 있다. 왜냐하면 프로그램 성능의 특징은 아주 직관적이지 못하다. 결과적으로 남에 눈에는 띄지 않고 말할수 많은 노력이 성능 향상을 위해 프로그램의 관련된 부분에 쏟아 부어 진다. 예를들어서 아마, 계산을 최소화 시키는 알고리즘과 데이터 구조가 프로그램에 적용 되다. 그렇지만 만약에 입출(I/O-bound)력 부분 적용된다면 저것은 허사가 된다. 증가되는 I/O 라이브러리는 아마 컴파일러에 의하여 바뀐 그 코드에 의해 교체될것이다. 그렇지만, 프로그램이 CPU-bound에 대한 사용이라면 또 이건 별로 중요한 포인터(관점)이 되지 않는 것이다.
대부분 프로그래머들은 그들의 프로그램에 관한 특성에 관하여 멍청한 직관력을 가지고 있다. 왜냐하면 프로그램 성능의 특징은 아주 직관적이지 못하다. 결과적으로 남에 눈에는 띄지 않고 말할수 많은 노력이 성능 향상을 위해 프로그램의 관련된 부분에 쏟아 부어 진다. 예를들어서 아마, 계산을 최소화 시키는 알고리즘과 데이터 구조가 프로그램에 적용 되다. 그렇지만 만약에 입출(I/O-bound)력 부분 적용된다면 저것은 허사가 된다. 증가되는 I/O 라이브러리는 아마 컴파일러에 의하여 바뀐 그 코드에 의해 교체될것이다. 그렇지만, 프로그램이 CPU-bound에 대한 사용이라면 또 이건 별로 중요한 포인터(관점)이 되지 않는 것이다.
저러한 상황에서, 만약 내가 느린 플그램이나 너무 많은 프로그램을 만나면 어떻게 해야하는가? 80-20 규칙은 프로그램의 랜덤 구역의 증가는 돕는데 썩좋지는 않다는 걸 의미한다. 사실, 프로그램은 성능 향상은 비직관적이다. 하지만 당신의 프로그램에서 단순한 랜덤 부분의 증가보다 성능의 병목 지점을 찾는 생각에 노력을 기울이는 것이 더 좋와 보이지는 않은 것이다. 자 그럼 일해 보실까요?
일을 할 그 부분은 실질적으로 당신의 프로그램의 20%로, 당신에게 고민을 안겨주는 부분이다. 그리고 끔찍한 20%를 찾는 방법은 프로그램 프로파일러(profiler:분석자)를 사용하는 것이다. 그렇지만 어떠한 프로파일러(profiler:분석자)도 못할일이다. 당신은 가장 관심 있는 직접적인 해결책을 내놓는 것을 원한다.예를 들자면 당신의 프로그램이 매우 느리다고 하자, 당신은 프로파일러(profiler:분석자)가 프로그램의 각각 다른 부분에서 얼마나 시간이 소비되는지에 관해서 말해줄껄 원한다. 당신이 만약 그러한 능률 관점으로 중요한 향상을 이룰수 있는 부분에 관해 촛점을 맞추는 방법만 알고 있다면 또한 전체 부분에서 효율성을 증대시키는 부분을 말할수있을 것이다.
프로파일러(profiler:분석자)는 각각의 구문이 몇번이나 실행되는가 아니면 각각의 함수들이 몇번이나 불리는거 정도를 알려주는 유틸리티이다. 성능(performance)관점에서 당신은 함수가 몇번 분리는가에 관해서는 그리 큰 관심을 두지 않을 것이다. 프로그램의 사용자 수를 세거나, 너무 많은 구문이 수행되어 불평을 받는 라이브러리를 사용하는 클라이언트의 수를 세거나, 혹은 너무 많은 함수들이 불리는 것을 세는 것은 다소 드문 일이기도 하다. 하지만 만약 당신의 소프트웨어가 충분이 빠르다면 아무도 실행되는 구문의 수에 관해 관여치 않는다. 그리고 만약 너무 느리면 반대겠지. (이후 문장이 너무 이상해서 생략, 바보 작성자)
몇번이나 구문이 실행되는가, 함수가 실행되는가는 때때로 당신의 소프트웨어 안의 모습을 이야기 해준다. 예를들어 만약 당신이특별한 형태의 객체를 수백개를 만든다고 하면, 생성자의 횟수를 세는것도 충분히 값어치 있는 일일 것이다. 게다가 구문과, 함수가 불리는 숫자는 당신에게 직접적인 해결책은 제시 못하겠지만, 소프트웨어의 한면을 이해하는데 도움을 줄것이다. 예를들어서 만약 당신은 동적 메모리 사용을 해결하기 위한 방법을 찾지 못한다면 최소한 몇번의 메모리 할당과 해제 함수가 불리는것을 아게되는것은 유용한 도움을 줄지도 모른다. (e.g., operators new, new[], delete and delete[] - Item 8참고)
물론,프로파일러(profiler:분석자)의 장점은 프로세스중 데이터를 잡을수 있다는 점이다. 만약 당신이 당신의 프로그램을 표현되지 않는 입력 값에 대하여 프로파일(감시 정도 의미로)한다고 하면, 프로파일러가 보여준 당신의 소프트웨어의 모습에서 보통의 속도와, 잘 견디는 모습을 보여준다면 - 그부분이 소프트웨어의 80%일꺼다. - 불만있는 구역에는 접근하지 않을 다는 의미가 된다. 프로파일은 오직 당신에게 프로그램의 특별난 부분에 관해서만 이야기 할수 있는걸 기억해라 그래서 만약 당신이 표현되지 않는 입력 데이터 값을 프로파일 한다면 당신은 겉으로 들어나지 않는 값에 대한 프로파일로 돌아가야 할것이다. 그것은 당신이 특별한 쓰임을 위하여 당신의 소프트웨어를 최적화 하는것과 비슷하다. 그리고 이것은 전체를 보는 일반적인 쓰임 아마 부정적인 영양을 줄것이다.
이런 결과들을 막는데 최선책은 당신의 소프트웨어에 가능한한 많은 데이터 들에게 프로파일을 시도하는것이다. 게다가 당신은 각 데이터들이 소프트웨어가 그것의 클라이언트들(혹은 최소한 가장 중요한 클라인트들에게라도)에게 사용방식을 잘 보여주도록 확신할수 있어야만 한다. 잘표현되는 데이터들은 얻기가 용이하다 왜냐하면 프로파일링 중에는 당신이 그들의 데이터를 사용할수 있기때문에 많은 클라이언트들이 좋기 때문이다. (뭔소리야. --;) 당신은 당신의 소프트웨어를 그들과 만나면서 조정(tuning)을 할것이고, 그것이 오직 당신이나 클라이언트들 양쪽에게 좋은 방법이다.
1.2. Item 17:Consider using lazy evaluation ¶
- Item 17:lazy evaluation의 쓰임에 대하여 생각해 보자.
능률(efficiency)의 관점에서 최고의 계산은 결코 아무것도 수행하지 않는것이다. 말이 좀 이상한가? 생각해 봐라 당신이 어떤 일도 필요없을때 이건 맞는거다. 왜 당신은 당신의 프로그램안에서 가장 처음에 그것을 수행하려 하는가? 그리고 만약 당신이 어떤 일을 수행하기를 원할때 당신은 그 코드의 실행(excuting)을 피할수는 없을까?
자 여기서의 열쇠(keyword)가 바로 lazy 이다.
우리가 어린이이고, 당신의 부모님들이 당신에게 방을 치우라고 이야기 했을때를 기억해 보자. 만약 당신이 나와 같다면 말이지 난 당장 "네" 하고 대답하고 아마도 다시 내가하던 다른 일을 할꺼다. 당신은 아마 방을 치우지 않겠지. 사실 방을 치우는 작업은 당신의 일의 우선순위에 대한 생각에서 마지막에 위치한다. - 그러니까. 당신의 부모님이 당신에 방에 다가오는 소리를 들을때 말이지. 그리고 나면 당신은 전속력으로 방으로 뛰어들어가 가능한한 가장 빨리 치운다. 만역 당신이 행운아라면 부모님들은 결코 체크를 안하시고 당신은이런 모든 치우는 귀찮은 작업을 보통 꺼린다.
이런 같은 관점을 이제 막 5년차 C++프로그래머에 대입 시켜본다. 컴퓨터 과학에서, 우리는 그러한 뒤로 미루기를 바로 lazy evaluation(구지 해석하면 필요시 연산, (최)후 연산, 늦은 연산정도라 할수 있겠다.)이라고 말한다. 당신이 lazy evaluation을 사용하면 당신의 클래스들이 최종적으로 원하는 결과가 나올 시간까지 지연되는 그런 상태로 코딩을 해야 한다. 만약 결과값을 결국에는 요구하지 않는다면, 계산은 결코 수행되지 않아야 한다. 그리고 당신의 소프트웨어의 클라이언트들과 당신의 부모님은 더 현명하지 않아야 한다.( 무슨 소리냐 하면, 위의 방치우기 이야기 처럼 부모님이나 클라이언트들이 lazy evaluation기법의 일처리로 해결을 하지 않아도 작업에 대한 신경을 안써야 한다는 소리 )
아마 당신은 내가 한 이야기들에 대하여 의문스로운 점이 있을것이다. 아마 다음의 예제들이 도움을 줄것이다. 자!, lazy evaluation은 어플리케이션 상에서 수많은 변화에 적용할수 있다. 그래서 다음과 같이 4가지를 제시한다.
1.2.1. Reference Counting (참조 세기) ¶
다음과 같은 코드를 생각해 봐라
~cpp class String { ... }; // 문자열 클래스 (이건 밑의 언급과 같이 표준 스트링 타입과 // 같이 사용 방식이 적용된다고 가정한다. 하지만 결코 존재하지는 않는다.) String s1 = "Hello"; String s2 = s1; // String 복사 생성자를 부른다.String 복사 생성자의 적용시, s2는 s1에 의하여 초기화 되어서 s1과 s2는 각각 "Hello"를 가지게된다. 그런 복사 생성자는 많은 비용 소모에 관계되어 있는데, 왜냐하면, s1의 값을 s1로 복사하면서 보통 heap 메모리 할당을 위해 new operator(Item 8참고)를 s1의 데이터를 s2로 복사하기 위해 strcpy를 호출하는 과정이 수행되기 때문이다. 이것은 eager evaluation(구지 해석하면 즉시 연산 정도 일것이다.) 개념의 적용이다.:s1의 복사를 수행 하는 것과, s2에 그 데이터를 집어넣는 과정, 이유는 String의 복사 생성자가 호출되기 때문이다. 하지만 여기에는 s2가 쓰여진적이 없이 새로 생성되는 것이기 때문에 실제로 s2에 관해서 저런 일련의 복사와, 이동의 연산의 필요성이 없다.
lazy 접근 방법은 좀더 훨씬 적은 수행을 이끈다. s1의 복사로 s2를 제공하기 대신에 s2가 s1의 값을 공유해 버리는 것이다.
지금 위에서 이렇게 접근하는 방식은 작고, 간단한 부분을 언급하는거에 불과하다 그래서, 누가 무엇을 공유했는지 알고 있고, 반환되는 값으로, 언급한 new와 복사에 추가비용되는 지출을 줄일수 있다. s1,s2가 공유하는 데이터 구조의 상태는 클라이언트들에게 명확하다. 그리고 그것은 확실히 다음에 제시될 예제같이 값을 쓰지 않고 읽기만을 요구할때는 아무런 걱정할 점이 없다.
지금 위에서 이렇게 접근하는 방식은 작고, 간단한 부분을 언급하는거에 불과하다 그래서, 누가 무엇을 공유했는지 알고 있고, 반환되는 값으로, 언급한 new와 복사에 추가비용되는 지출을 줄일수 있다. s1,s2가 공유하는 데이터 구조의 상태는 클라이언트들에게 명확하다. 그리고 그것은 확실히 다음에 제시될 예제같이 값을 쓰지 않고 읽기만을 요구할때는 아무런 걱정할 점이 없다.
~cpp cout << s1; // s1의 값을 읽는다. cout << s1 + s2; // s1과 s2의 값을 읽는다.사실, 값을 공유하는 시간은 둘중 아무거나 값이 수정되어 버릴때 다른점이 발생하기 전까지만 유효한 것이다. :ㅣ그런데 양쪽이 다 바뀐게 아니라 한쪽만 바뀌는 이런 지적은 중요한것이다. 다음과 구문처럼
~cpp s2.convertToUpperCase();이건 s2의 값만을 바꾸고 s1에는 영향을 끼치지 않은 요구로, 매우 치명적이다.
이와 같은 구문의 사용으로, String의 convertToUpperCase 함수를 적용하면, s2의 값의 복사본을 만들어야 하고, 수정되기전에 s2에 그걸 s2의 종속되는 데이터로 만들어야 한다. convertToUpperCase 내부에 우리는 lazy 상태가 더이상 지속되지 않도록 하는 코드를 넣어야 한다.:s2가 마음대로 다룰수 있도록 s2의 공유된 값의 사본을 복사해야 한다. 반면에 만약 s2가 결코 수정되지 않을 것이라면, 이러한 s2만의 값을 복사하는 일련의 과정이 필요 없을 것이다. 그리고 s2가 존재하는 만큼 값도 계속 존재해야 한다. 만약 더 좋게, s2가 앞으로 결코 변하지 않는다면, 우리는 결코 그것의 값에 대한 노력을 할필요가 없을 것이다.
값의 공유에 관하여 좀더 자세하게 이 문제에 논의를 제공할 부분은 Item 29(모든 코드가 들어있다.)에 있다. 하지만 그 생각 역시 lazy evaluation이다.:결코 당신이 정말로 어떤것을 필요하기 전까지는 그것의 사본을 만드는 작업을 하지 않것. 일단 그보다 lazy 해져봐라.- 어떤이가 당신이 그것을 제거하기 전까지 같은 자원을 실컷 사용하는것. 몇몇 어플리케이션의 영역에서 당신은 종종 저러한 비합리적 복사의 과정을 영원히 제거해 버릴수 있을 것이다.
1.2.2. Distinguishing Read from Writes ( 읽기와 쓰기의 구분 ) ¶
reference-counting 을 토대로한 문자열의 구현 예제를 조금만 생각해 보면 곧 lazy evaluation의 방법중 우리를 돕는 두번째의 것을 만나게 된다. 다음 코드를 생각해 보자
~cpp String s = "Homer's Iliad"; // 다음 문자열이 reference-counting으로 // 구현되었다고 생각하자 ... cout << s[3]; // operator []를 호출해서 s[3]을 읽는다.(read) s[3] = 'x'; // operator []를 호출해서 s[3]에 쓴다.(write)첫번째 operator[]는 문자열을 읽는 부분이다,하지만 두번째 operator[]는 쓰기를 수행하는 기능을 호출하는 부분이다. 여기에서 읽기와 쓰기를 구분할수 있어야 한다.(distinguish the read all from the write) 왜냐하면 읽기는 refernce-counting 구현 문자열로서 자원(실행시간 역시) 지불 비용이 낮고, 아마 저렇게 스트링의 쓰기는 새로운 복사본을 만들기 위해서 쓰기에 앞서 문자열 값을 조각내어야 하는 작업이 필요할 것이다.
DeleteMe ) 단란의 후반이 약간 이상하다.
이것은 우리에게 적용 관점에서 상당히 난제이다. 우리가 원하는 것에 이르기 위하여 operator[] 안쪽에 각기 다른 작업을 하는 코드가 필요하다.(읽기와 쓰기에 따라서 따로 작동해야 한다.) 어떻게 우리는 operator[]가 읽기에 불리는지 쓰기에 불리는지 결정할수 있을까? 이런 잔인한 사실은 우리를 난감하게 한다. lazy evaluation의 사용과 Item 30에 언급된 proxy 클래스(위임 클래스, DP에서의 역할과 비슷할것이라 예상) 는 우리가 수정을 위하여 읽기나 쓰기 행동을 하는지의 결정을 연기하게 한다.
이것은 우리에게 적용 관점에서 상당히 난제이다. 우리가 원하는 것에 이르기 위하여 operator[] 안쪽에 각기 다른 작업을 하는 코드가 필요하다.(읽기와 쓰기에 따라서 따로 작동해야 한다.) 어떻게 우리는 operator[]가 읽기에 불리는지 쓰기에 불리는지 결정할수 있을까? 이런 잔인한 사실은 우리를 난감하게 한다. lazy evaluation의 사용과 Item 30에 언급된 proxy 클래스(위임 클래스, DP에서의 역할과 비슷할것이라 예상) 는 우리가 수정을 위하여 읽기나 쓰기 행동을 하는지의 결정을 연기하게 한다.
1.2.3. Lazy Fetching ( 늦은 가져오기) ¶
lazy evaluation에서 다룰 세번째의 주제로, 당신이 많은 필드로 이루어진 큰 객체들을 사용하는 프로그램을 가지고 있다고 상상해 봐라. 그런 객체들은 반드시 프로그램이 실행때 유지되며, 나중에는 데이터 베이스 안에 저장된어진다. 각각의 객체는 각 객체를 알아볼수 있고, 유일성을 보장하는 데이터 베이스로 부터 객체를 불러올때 종류를 알아 볼수 있는, 식별자(identifier)를 가지고 있다.(OODB 인가.) :
~cpp class LargeObject { // 크고, 계속 유지되는 객체들 public: LargeObject(ObjectID id); // 디스크에서 객체의 복구(부르기) cosnt string& field1() const; // 필드상의 값1 int field2() const; // 필드상의 값2 double field3() const; // ... const string& field4() const; const string& field5() const; ... };자 그럼 디스크에서 복구(자료를 부르기)되어지는 LargeObject의 비용을 생각해 보자:
~cpp void restoreAndProcessObject(ObjectID id) // 객체 복구 { LargeObject object(id); ... }LargeObject 인스턴스들이 크기 때문에 그런 객체에서 모든 데이터를 얻는 것은, 만약에 특별히 데이터 베이스가 외부에 네크워크 상에서 자료 로드가 있다면 데이터 베이스의 작업은 비쌀것이다. 몇몇 상황에서 모든 데이터 베이스를 읽어들이는 비용은 필요가 없다. 예를 들어서 다음의 어플리케이션의 종류에 관하여 생각해 보자.
~cpp void restoreAndProcessObject(ObjectID id) { LargeObject object(id); if (object.field2() == 0) { cout << "Object " << id << ": null field2.\n"; } }이런 경우에서는 오직 field2의 값만을 요구한다. 따라서 다른 필드를 로드하는 작업은 필요없는 작업이 되어 진다.
lazy 로의 접근에서 이런 문제는 LargeObject가 만들어 질때 디스크에서 아무런 데이터를 읽어 들이지 않는 것이다. 대신에 오직 객체의 "껍데기"(shell)만 만들어 주고, 데이터는 객체 내부에서 특정 데이터를 필요로 할때만 데이터 베이스에서 데이터를 복구하는 것이다. 여기 그런 관점에서 "damand-paged" 방식으로 객체 초기화를 적용한 방법이 있다.
~cpp class LargeObjectP pulic: LargeObject(ObjectID id); const string& field1() const; int field2() const; double field3() const; const string& field4() const; ... private: ObjectID oid; mutable string *field1Value; // 앞으로의 "mutable"에 관한 토론을 보라 mutable int *field2Value; mutable double *field3Value; mutable string *field4Value; ... }; LargeObject::LargeObject(ObjectID id):oid(id), field1Value(0), field2Value(0), field3Value(0), ... {} const string& LargeObject::field() const { if (field1Value == 0){ field1의 데이터를 읽기 위하여 데이터 베이스에서 해당 데이터를 가지고 와서 field1Value 가 그것을 가리키게 한다. } return *field1Value; }객체의 각 필드는 필요한 데이터의 포인터로 표현되어 있고, LargeObject의 생성자는 null로 초기화 된다. 그런 null 포인터는 아직 데이터 베이스에서 해당 필드의 정보를 안읽었다는 걸 의미한다. 데이터를 접근하기 전에 LargeObject의 각 멤버 함수는 반드시 이 필드의 포인터를 검사한다. 만약 포인터가 null이라면 데이터를 사용하기 전에 반드시 데이터 베이스에서 읽어 온다.
lazy fetching을 적용 하면, 당신은 반드시 field1과 같은 const멤버 함수를 포함하는 어떠한 멤버 함수에서 실제 데이터 포인터를 초기화하는 과정이 필요한 문제가 발생한다.(const를 다시 재할당?) 하지만 컴파일러는 당신이 const 멤버 함수의 내부에서 데이터 멤버를 수정하려고 시도하면 까다로운(cranky) 반응을 가진다. 그래서 당신은 "좋와, 나는 내가 해야 할것을 알고있어" 말하는 방법을 가지고 있어야만 한다. 가장 좋은 방법은 포인터의 필드를 mutable로 선언해 버리는 것이다. 이것의 의미는 어떠한 멤버 함수에서도 해당 변수를 고칠수 있다는 의미로, 이렇게 어떠한 멤버 함수내에서도 수행할수 있다. 이것이 LargeObject안에 있는 필드들에 mutable이 모두 선언된 이유이다.
mutable 키워드는 최근에 C++에 추가되어서, 당신의 벤더들이 아직 지원 못할 가능성도 있다. 지원하지 못한다면, 당신은 또 다른 방법으로 컴파일러에게 const 멤버 함수 하에서 데이터 멤버들을 고치는 방안이 필요하다. 한가지 가능할 법인 방법이 "fake this"의 접근인다. "fake this"는 this가 하는 역할처럼 같은 객체를 가리키는 포인터로 pointer-to-non-const(const가 아닌 포인터)를 만들어 내는 것이다. (DeleteMe 약간 이상) 당신이 데이터 멤버를 수정하기를 원하면, 당신은 이 "fake this" 포인터를 통해서 수정할수 있다.:
~cpp class LargeObject { public: const string& field1() const; // 바뀌지 않음 ... private: string *field1Value; // mutable로 선언되지 않았다. ... // 그래서 과거 컴파일러는 이걸 허용한다. }; const string& LargeObject::field1() const { // 자 이것이 fake This 인데, 저 포인터로 this에 대한 접근에서 const를 풀어 버리는 역할을 한다. // LargeObject* 하고 const가 뒤에 붙어 있기 때문에 LargeObject* 자체는 const가 아닌 셈이다. LargeObject * const fakeThis = const_cast<LargeObject* const>(this); if( field1Value == 0){ fakeThis->field1Value = // fakeThis 가 const가 아니기 때문에 데이터베이스에 접근해서 // 이러한 시도는 합당하다. 포인터를 넘기는 부분 } return * field1Value; }이 함수는 *this의 constness성질을 부여하기 위하여 const_cast(Item 2참고)를 사용했다.만약 당신이 const_cast마져 지원 안하면 다음과 같이 해야 컴파일러가 알아 먹는다.
~cpp const string& LargeObject::field1() const { LargeObject * const fakeThis = (LargeObject* const)(this); ... }
자, 그럼 다시 한번 LargeObject내의 포인터들에 관하여 생각해 보자. 사용하기전에 각각의 포인터들을 검사하는 것에 비해서, 모든 포인터들이 null로 초기화 되어 있는것은 에러의 가능성을 가지고 있다. 다행히도, 이런 우려는 Item28의 smart pointers의 이용으로 편이성을 제시한다. 만약 LargeObject내부에서 smart pointer를 사용한다면 당신은 아마도 더이상 포인터를 mutable하게 선언할 필요가 없을것이다. 당신이 mutable을 필요로 하는 상황이, smart pointer클래스들의 적용으로 가기 때문에 위의 내용은 좀 임시적인것이다. 이런 문제에 관해 한번 생각해 봐라
- Lazy Expression Evaluation ( 표현을 위한 게으른 연산 )
~cpp template<class T> class Matrix { ... }; Matrix<int> m1(1000, 1000); // 굉장히 큰 int형 1000x1000배열을 선언한거다. Matrix<int> m2(1000, 1000); ... Matrix<int> m3 = m1 + m2; // 그리고 그 둘을 더한다 굉장한 연산이 필요해진다.보통 operator+에 대한 구현은 아마 eager evaluation(즉시 연산) 이 될것이다.;이런 경우에 그것은 아마 m1과 m2의 리턴 값을 대상으로 한다. 이 계산(1,000,000 더하기)에 적당한 게산양과, 메모리 할당에 비용 이 모드것이 수행되어져야 함을 말한다.
lazy evaluaion 방법에서는 저건 너무 엄청난 수행을 하는 방법이라 하고, 그래서 그것을 수행하지 않는다. 대신에 m3내부에 m1과 m2의 합을 했다는 것만을 기럭해 둔다. 그런 자료 구조는 아마도 m1과 m2나 그이상의 더하기를 하기 위한 포인터 외에는 아무런 정보를 유지할 필요가 없을 것이다. 명백히 이건 m1,m2에 대한 실제 더하기보다 훨씬 빠르고 말할것도 없이 훨씬 적은 메모리를 사용할 것이다.
프로그램이 m3이후에 다음과 같은 짓(이건 예제가 열받게해서 이렇게 쓴다.)을 저지른다.
~cpp Matrix<int> m4(1000, 1000); ... // 아까 위에서 했던 m4에 어떠한 값을 넣는 코드들이다. m3 = m4 * m1;우리는 이제 아까 m1, m2의 합인 m3를 잃어버렸다.( 그리고 이건 합의 계산 비용을 줄인다는 의미도 된다.) 그것에는 m4,m1의 곱이 새로운 값으로 기억되어 진다. 말할 필요도 없이 이제 곱은 수행안하는 거다. 왜냐? 우리는 lazy 하니까. ~
사실 이건 멍청한 프로그래머가 두 행렬의 합을 계산하고, 그것을 사용하지 않아서 얻은 이점을 노린 억지로 만들어낸 예제 같이 보인다. 멍청한 프로그래머는 필요도 하지 않은 계산을 수행한 것이다. 하지만 유지보수 중에 보면, 이런 필요없는 계산을 이행하는 수행코드는 그리 희귀하지는 않다.
하지만, lazy evaluation이 치룬 시간이 오직 저런 상태일 뿐이라면, "엄청난 계산을 요구한다"라는 문제가 더 커질것이라고 생각하기는 어렵다.의 필요성이 좀더 일반적인 시나리오는 우리가 오직 계산에서의 일부가 필요한 경우이다. 예를 들자면 우리가 m3를 m1과 m2의 합으로 초기화 했다고 가정하고 다음과 같은 코드가 있다면
~cpp cout << m3[4]; // m3의 4번째 열만을 요구한다.확실히 우리는 이제 lazy 상태를 벗어나야 함을 알수 있다.-우리는 m3의 4번째 열에 대하여 계산된 값을 가지고 있어야 한다. 그러나, 역시나 너무나 지나친 것이다. 우리는 m3의 4번재 열을 계산해야만 하는건 말할 필요도 없다.:m3는 그것이 필요해지기 전까지는 계산할필요가 없다. 하지만 행운으로 그렇게 할 필요가 없을것이다.
DeleteMe ) 내용이 이상하다. 보강 필요
어떻게 행운이냐구? 행렬 계산의 분야에 대한 경험이 우리의 이러한 코드에 대한 노력에 가능성을 준다. 사실 lazy evaluation은 APL이라는 것에 기초하고 있다. APL은 1960년대에 상호 작용의(interactive) 쓰임을 위하여 행렬 계산이 필요한 사람들에 의하여 개발된 것이다. 현재보다 떨어진 수행능력을 가진 컴퓨터에서 APL은 더하고, 곱하고, 심지어 커다란 행렬을 직접 나눈는 것처럼 보이게 하였다. 그것에는 lazy evaluation이라는 방법이었다. 그 방법은 일반적으로 보통 효율적이었다. 왜냐하면 APL 사용자가 보통 더하고, 곱하고 나누는 것을 그것의 행렬의 조각들을 필요로 하고, 전체의 결과가 필요하기 전까지 수행하지 않는다. APL 은 lazy evaluation을 사용해서 행렬상의 결과를 정확히 알 필요가 있을때까지 게산을 지연시킨다. 그런 다음 오직 필요한 부분만을 계산한다. 실제로 이것은 과거 열악한 컴퓨터의 능력하에서 사용자들이 계산 집약적인(많은 행렬 계산을 요하는) 문제에 관하여 상호적으로(결과값과 수행 식간에 필요 값을 위해서 최대한 실제 연산을 줄여나가게) 수행된다.현재의 기계도 빨라졌지만, 데이터들이 커지고, 사용자들은 참을성이 줄어들기 때문에 요즘에도 이런 lazy evaluation의 장점을 이용한 행렬 연산 라이브러리를 사용한다.
어떻게 행운이냐구? 행렬 계산의 분야에 대한 경험이 우리의 이러한 코드에 대한 노력에 가능성을 준다. 사실 lazy evaluation은 APL이라는 것에 기초하고 있다. APL은 1960년대에 상호 작용의(interactive) 쓰임을 위하여 행렬 계산이 필요한 사람들에 의하여 개발된 것이다. 현재보다 떨어진 수행능력을 가진 컴퓨터에서 APL은 더하고, 곱하고, 심지어 커다란 행렬을 직접 나눈는 것처럼 보이게 하였다. 그것에는 lazy evaluation이라는 방법이었다. 그 방법은 일반적으로 보통 효율적이었다. 왜냐하면 APL 사용자가 보통 더하고, 곱하고 나누는 것을 그것의 행렬의 조각들을 필요로 하고, 전체의 결과가 필요하기 전까지 수행하지 않는다. APL 은 lazy evaluation을 사용해서 행렬상의 결과를 정확히 알 필요가 있을때까지 게산을 지연시킨다. 그런 다음 오직 필요한 부분만을 계산한다. 실제로 이것은 과거 열악한 컴퓨터의 능력하에서 사용자들이 계산 집약적인(많은 행렬 계산을 요하는) 문제에 관하여 상호적으로(결과값과 수행 식간에 필요 값을 위해서 최대한 실제 연산을 줄여나가게) 수행된다.현재의 기계도 빨라졌지만, 데이터들이 커지고, 사용자들은 참을성이 줄어들기 때문에 요즘에도 이런 lazy evaluation의 장점을 이용한 행렬 연산 라이브러리를 사용한다.
laziness(게으름)은 때로 시간을 아끼는대 실패한다. m3가 이런식으로 쓰이면:
~cpp cout << m3; // m3의 모든것을 찍는다.뭐 끝났다. m3를 위한 모든 값을 가지고 있어야 한다. 비슷하게 m3가 의존하는 행렬들중에 수정되는것 이 있어도, 즉시 계산을 필요로 한다.
~cpp m3 = m1 + m2; // m3가 m1,m2의 합인걸 기억하라 m1 = m4; // 이제 m3는 m2와 과거 m1의 합이 된다.그러므로 몇가지의 m1에 대한 할당이 m3를 변화시키지 않는다는 확신을 가지고 있어야 한다. Matrix<int>의 내부에 할당된 operator 내부에 m3의 값이 m1의 계산 이전에 계산되어 있거나, m1의 과거 값에 대한 복사본을 가지고 있고 m3는 그것에 의존해야 한다. 다른 함수들도 이러한 행렬의 변경을 위하여 다른 형식의 함수들도 이런 비슷한 것을 감안해야 할것이다.
각 값 간의 의존성과,;데이터 구조의 유지를 위하여, 값들, 의존성이나 두가지의 결합 방법을 저장해야 한다.; 그리고 많은 수치 계산이 필요한 분야에서 복사, 더하기 할당, 같은 operator의 overload 것이 필요하다. 반면에 lazy evaluation의 적용은 프로그램 실행중에서 정말 많은 시간들과 많은 자원들을 아낄수 있다. 그래서 lazy evaluation의 존재를 정당화 시켜 줄것이다.
- Summary (요약)
C++에 알맞는 lazy evaluation은 없다. 그러한 기술은 어떠한 프로그래밍 언어에도 적용 될수 있다. 그리고 몇몇 언어들-APL, 몇몇 특성화된 Lisp, 가상적으로 데이터 흐름을 나타내는 모든 언어들-는 언어의 한 중요한 부분이다. 그렇지만 주요 프로그래밍, C++같은 언어들은 eager evaluation를 기본으로 채용한다. C++에서는 사용자가 lazy evaluation의 적용을 위해 매우 적합하다. 왜냐하면 캡슐화는 클라이언트들을 꼭 알지 못해도 lazy evaluation의 적용을 가능하게 만들어 주기 때문이다.
이제까지 언급했던 예제 코드들을 다시 한번 봐라 당신은 클래스 인터페이스만이 주어진다면 그것이 eager, lazy인지 알수는 없을 것이다. 그것의 의미는 eager evaluation도 역시 곧바로 적용 가능하고, 반대도 가능하다는 의미이다. 만약, 연구를 통해서 클래스의 구현에서 병목 현상을 보이는 부분이 보인다면, 당신은 lazy evaluation의 전략에 근거한 코드들을 적용 할수 있을 것이다. 당신의 클라이언트들은 이러한 변화가 성능의 향상으로 밖에 보이지 않는다. 고객(클라이언트들)들이 좋와하는 소프트웨어 향상의 방법, 당신이 자랑스로워하는 lazy가 될수 있다. (DeleteMe 모호)
1.3. Item 18: Amortize the cose of expected computations. ¶
- Item 18: 예상되는 연산의 값을 계산해 두어라.
자, 다음 예제를 생각해 보자. 수치 데이터의 큰 calloections을 나타네는 클래스들을 위한 템플릿이다.
~cpp template<class NumericalType> class DataColletion { public: NumericalType min() const; NumericalType max() const; NumericalType avg() const; ... }min, max, avg에 함수는 현재의 해당 collection의 최소값, 최대값 평균을 반환하는 값이라고 생각해라, 여기에서 이들이 구현될수 있는 방법은 3가지 정도가 있다. eager evaluation(즉시연산)을 이용해서 min, max, avg가 호출될때마다 해당 연산을 하는 방법. lazy evaluation(게으른연산)을 해서 해당 함수값이 반환하는 값이, 실제로 연산에 필요할때 마지막에 계산에서 연산해서 값을 얻는 방법. 그리고 over-eager evaluation(미리연산)을 이용해서 아예 실행중에 최소값, 최대값, 평균값을 collection내부에 가지고 있어서 min, max, avg가 호출되면 즉시 값을 제공하는 방법-어떠한 계산도 필요 없이 그냥 즉시 돌리는거다. 만약 min, max, avg가 자주 호출된다면 collection의 최소값, 최대값, 평균값을 이용하는 함수들이 collection 전역에 걸쳐서 계산을 필요로 할수 있다. 그렇다면 이런 계산의 비용은 eager,lazy evaluaton(게으른연산, 즉시연산)에 비하여 저렴한 비용을 지출하게 될것이다.(필요값이 즉시 반환되니)
이런 일을 행하는데에 가장 간단한 방법은 이미 계산된 값을 저장시켜 놓고, 다시 필요로할때 쓰는거다. 예를들어 당신이 직원들에 관한 정보를 제공하는 프로그램을 만든다고 가정하자, 그리고 당신이 자주 쓰인다고 예상할수 있는 정보중 하나는 직원들의 개인방(사무실 or 침실 or 숙소) 번호 이다. 거기에 직원들의 정보는 데이터 베이스에 저장되어 있다고 가정한다. 하지만 대다수(당신이 작성하는거 말고) 프로그램을 위하여 직원들의 개인방 번호는 잘 쓰이지 않는다. 그래서 데이터 베이스에서 그것을 찾는 방법에 관한 최적화가 되어 있지 않다. 당신은 직원들의 개인방 번호를 반복적으로 요구하는 것에 대한 데이터 베이스가 받는 과도한 스트레스에 어플리케이션단에서 특수한 구조로 만드는 걸 피하려면, findCubicleNumber 함수로서 개인방 번호를 캐시(임시저장) 시켜 놀수 있다. 이미 가지고 있는 개인방 번호에 대하여 연속적으로 불리는 요구는 데이터 베이스에 매번 쿼리(query)를 날리는것보다는 캐쉬를 조사하여 값을 만족 시킬수 있다.
여기 findCubicleNumber를 적용시키는 한 방법이 있다.;그것은 지역(local)캐쉬로 STL의(Standard Template Library-Item 35 참고) map 객체를 사용한다.
~cpp int findCubicleNumber(const string& employeesName) { // static으로 map을 선언하는 과정 이 맵이 local cashe이다. typedef map<string, int> CubicleMap; static CubicleMap cubes; // 해당 직원 이름을 바탕으로 cache에서 찾는 과정 // STL interator "it"은 해당 entry를 찾는다. CubicleMap::iterator it = cubes.find(employeeName); // 만약 아무런 entry를 찾을수 없다면, "it"의 값은 cubes.end이다. // 그런 경우에는 db에서 자료를 가지고 와야 한다. if(it == cubes.end()){ int cubicle = 직원 이름의 개인방 번호를 데이터 베이스에서 얻어오는 과정 cubes[employeeName] = cubicle; // 추가 return cubicle; } else { // "it" 포인터는 정확한 cache entry를 가리키며 cubicle번호는 두번째 인자라 // 이런 방법으로 얻는다. return (*it).second; } }STL코드를 자세히 알고 싶어서 촛점을 벗어나지 말아라. Item 35 보면 좀 확실히 알게 될것이다. 대신에 이 함수의 전체적인 기능에 촛점을 맞추어 보자.현재 이 방법은 비교적 비싼 데이터 베이스의 쿼리(query)문에대한 비용대신에 저렴한 메모리상의 데이터 베이스 구조에서 검색을 하는 것으로 교체하는걸로 볼수 있다. 개인 방번호에 대한 호출이 한벙 이상일때 findCubicleNumber는 개인방 번호에 대한 정보 반환의 평균 비용을 낮출수 있다. (한가지 조금 자세히 설명하자면, 마지막 구문에서 반환되는 값 (*it).second이 평범해 보이는 it->second 대신에 쓰였다. 왜? 대답은 STL에 의한 관행이라고 할수 있는데, 반환자(iterator)인 it은 객체이고 포인터가 아니라는 개념, 그래서 ->을 it에 적용할수 있다라는 보장이 없다. STL은 "."과 "*"를 interator상에서 원한다. 그래서 (*it).second라는 문법이 약간 어색해도 쓸수 있는 보장이 있다.)
캐시(cashing)는 예상되는 연산 값을 기록해 놓는 하나의 방법이다. 미리 가지고 오는 것이기도 하다. 당신은 대량의 계산을 줄이는 것과 동등한 효과를 얻을것이라 생각할수 있다. 예를들어서, Disk controller는 프로그래머가 오직 소량의 데이터만을 원함함에도 불구하고 데이터를 얻기위해 디스크를 읽어 나갈때, 전체 블록이나 읽거나, 전체 섹터를 읽는다. 왜냐하면 각기 여러번 하나 두개의 작은 조각으로 읽는것보다 한번 큰 조각의 데이터를 읽는게 더 빠르기 때문이다. 게다가, 이러한 경우는 요구되는 데이터가 한곳에 몰려있다는 걸 보여주고, 이러한 경우가 매우 일반적이라는 것 역시 반증한다. 이 것은 locality of reference (지역 데이터에 대한 참조, 여기서는 데이터를 얻기위해 디스크에 직접 접근하는걸 의미하는듯) 가 좋지 않고, 시스템 엔지니어에게 메모리 케쉬와, 그외의 미리 데이터 가지고 오는 과정을 설명하는 근거가 된다.
뭐시라?(Excuse me?) 당신은 disk controller와 CPU cash같은 저 밑에서 처리(low-level)하는 처리하는 일에 관해서는 신경 안쓰는 거라고? 걱정 마시라(No problem) 미리 가져오기(prefetching) 당신이 높은 수준(high-level)에서 할때 역시 야기되는 문제이니까. 예를들어, 상상해 봐라 당신은 동적 배열을 위하여 템플릿을 적용했다. 해당 배열은 1에서 부터 자동으로 확장되는 건데, 그래서 모든 자료가 있는 구역은 활성화된 것이다.: (DeleteMe 좀 이상함)
~cpp template<class T> // 동적 배열 T에 관한 클래스 템플릿 class DynArray { ... }; DynArray<double> a; // 이런 관점에서 a[0]은 합법적인 // 배열 인자이다. a[22] = 3.5; // a는 자동적으로 확장되었으며, // 현재는 0-22까지의 영역을 가진다. a[32] = 0; // 다시 확장되며 이제 0-32까지다.어떻게 DynArray 객체가 필요할때 마다 스스로 확장되는 걸까? 곧장 생각할수 있는건 그것이 새로운 메모리가 필요될때만 할당되고는 것이다 이런것 처럼 말이다.
~cpp templace<class T> T& DynArray<T>::operator[](int index) { if (index < 0) { 예외를 던진다.; } if (index > 현재 인덱스의 최대값){ new를 호출해서 충분한 메모리를 확보한다. 그렇게 해서 index를 활성화 시킨다.; } return 배열의 인덱스 인자를 반환한다.이러한 접근은 new를 배열의 증가 때만 부르는 간단한 방법 이지만 new는 operator new(Item 8참고)를 부르고, operator new(그리고 operaotr delete)는 보통 이 명령어들은 비용이 비싸다. 그것의 이유는 일반적으로 OS, 시스템 기반의 호출(System call)이 in-process 함수호출 보다 느린게 되겠다. (DeleteMe OS를 배워야 확실히 알겠다 이건) 결과적으로 가능한 system 호출(system call)을 줄여 나가야 한다.
over-eager evaluation(선연산,미리연산) 전술은 이 것에대한 답을 제시한다.:만약 우리가 index i로서 현재의 배열상의 크기를 늘리려면, locality of reference 개념은 우리가 아마 곧 index i보다 더 큰 공간의 필요로 한다는걸 이야기 한다. 이런 두번째 (예상되는)확장에 대한 메모리 할당의 비용을 피하기 위해서는 우리는 DynArray의 i의 크기가 요구되는 것에 비해서 조금 더 많은 양을 잡아서 배열의 증가에 예상한다. 그리고 곧 있을 확장에 제공할 영역을 준비해 놓는 것이다. 예를 들어 우리는 DynArray::operator[]를 이렇게 쓸수 있다.
~cpp template<class T> T& DynArray<T>::operator[](int index) { if (index < 0) 예외를 던진다.; if (index > 현재 인덱스의 최대값){ int diff = index - 현재 인덱스의 최대값; 알맞은 메모리를 할당한다. 그래서 index+diff가 유효하게 한다. } return 현재 인덱스가 가리키는 인자; }이 함수는 두번 충분한 양의 배열을 각각 필요할때 할당한다. 만약 우리가 앞서 이야기한 쓰임의 시나리오대로 진행 된다면, 아마 DynArray는 그것이 두번의 논리적 크기의 확장을 할지라도 오직 메모리를 한번만 할당할 것이다.:
~cpp DynArray<double> a; // 오직 a[0]만이 유효하다. a[22] = 3.5; // 현재 index 44정도의 저장 공간을 // 할당하며 a의 논리적 공간은 23이다. a[32] = 0; // a의 논리적 공간은 이제 a[32]이다. 하지만 // 44의 저장공간이 있기에 확장하지 않는다.만약 다시 확장이 필요하다면 44보다 크지 않는다면, new에 의한 높은 비용을 치루지 않는다.
이번 아이템은 일반적인 사용을 다루었다. 그리고 속도 향상은 상응 하는 메모리 비용을 지불을 해야만 할수 있다. 최대값, 최소값, 평균을 감안해서 요구되는 여분의 공간을 유지한다. 하지만 그것은 시간을 절약한다. cach 결과는 좀더 많은 메모리의 공간을 요구하지만 다시 할당되는 부분의 시간과 비용을 줄여서 비용을 절약한다. 미리 가지고 오고(prefetching)은 미리 가지고 와야 할것에 대한 공간을 요구하지만, 매번 그 자원에 접근해야 하는 시간을 줄여준다. 이러한 이야기(개념)은 Computer Science(컴퓨터 과학)에서 오래된 이야기 이다.:일반적으로 시간 자원과 공간 자원과의 교환(trade). (그렇지만 항상 이런 것이 가상 메모리와 캐쉬 페이지에 객체를 만드는것이 참은 아니다. 드문 경우에 있어, 큰 객체의 만드는 것은 당신의 소프트웨어의 성능(performance)을 향상 시킬 것이다. 왜냐하면 당신의 활성화 요구에 대한 활동이 증가하거나, 당신의 캐쉬에 대한 접근이 줄어 들또 혹은 둘다 일때 말이다. 당신은 어떻게 그러한 문제를 해결할 방법을 찾을 것인가? 상황을 점검하고 궁리하고 또 궁리해서 그문제를 해결하라(Item 16참고).)
이번 아이템에서의 나의 충고-caching과 prefetching을 통해서 over-eager의 전략으로 예상되는 값들의 미리 계산 시키는것-은 결코 item 17의 lazy evaluation(늦은 계산)과 반대의 개념이 아니다. lazy evaluation의 기술은 당신이 항상 필요하기 않은 어떠한 결과에대한 연산을 반드시 수행해야만 할때 프로그램의 효율성을 높이기 위한 기술이다. over-eager evaluation은 당신이 거의 항상 하는 계산의 결과 값이 필요할때 프로그램의 효율을 높여 줄것이다. 양쪽 모두다 eager evaluation(즉시 계산)의 run-of-the-mill(실행의 비용) 적용에 비해서 사용이 더 어렵다. 그렇지만 둘다 프로그램 많은 노력으로 적용하면 뚜렷한 성능 샹항을 보일수 있다.
1.4. Item 19:Understand the orgin of temporary objects. ¶
- Item 19:임시 객체들의 기본을 이해하자.
~cpp template<class T> void swap(T& object1, T& object2) { T temp = object; object1 = object2; object2 = temp; }자 여기서 temp를 보통 "temporary" 라고 부른다. 그렇지만 C++에 관점에서는 temp는 반드시 temporary라고 규정지을수 없다. 그것은 함수내에 존재하는 단순한 지역 객체일 뿐이다.
C++ 내에서의 진짜 temporary객체는 보이지 않는다.-이게 무슨 소리인고 하니, 소스내에서는 보이지 않는다는 소리다. temporary객체들은 non-heap 객체로 만들어 지지만 이름이 붙지를 않는다. (DeleteMe 시간나면 PL책의 내용 보충) 단지 이름 지어지지 않은(unnamed)객체는 보통 두가지 경우중에 하나로 볼수 있는데:묵시적(implicit) 형변환으로 함수호출에서 적용되고, 성공시에 반환되는 객체들. 왜, 그리고 어떻게 이러한 임시 객체(temporary objects)가 생성되고, 파괴되어 지는지 이해하는 것은 이러한 생성과 파괴 과정에서 발생하는 비용이 당신의 프로그램의 성능에 얼마나 성능을 끼칠수 있는가 알아야 하기때문에 중요한 것이다.
임시 객체(temporary objects)가 함수 호출이 성공된후 만들어지는 경우를 첫번째로 생각해 보자. 이것은 함수에 인자를 전달할때 서로간에 인자들의 형(type)가 맞지 않게 묶여(bind)진 경우에 일어 난다. 예를 들자면, 다음과 같은 문자열에서 어느 한글자가 출현하는 객수를 세는 함수를 생각해 보자
~cpp // 다음 함수는 str안에서 ch가 몇게 있는가 반환한다. size_t countChar(const string& str, char ch); char buffer[MAX_STRING_LEN]; char c; // string과 char를 받아 들인다. setw 는 글자를 읽을때 // 오버 플로우(overflow)를 방지하는 목적으로 쓰인다. cin >> c >> setw(MAX_STRING_LEC) >> buffer; cout << "There are " << countChar(buffer, c) << " occurrences of the charcter " << c << " in " << buffer << endl;countChar을 호출하는 곳을 보라. 처음에 구문에서 char 배열이 함수로 전달된다. 하지만 함수의 인자는 const string& 이다. 이런 호출은 오직 형(type)이 알맞지 않은것이 제거되거나 당신의 컴파일러는 훌륭히도 string 형의 임시 객체(temporary object)를 만들어서 그러한 맞지 않는 형문제를 제가하면 성공할수 있다. 그러한 임시 객체는 string 생성자가 buffer인자를 바탕으로 초기화 된다. 그러면 constChar의 str인자는 임시(temporary) string 객체를 받아들인다.(bind-bound) countChar이 반환될때 임시(temporary)객체는 자동 소멸된다.
이러한 편한(위험 하지만-Item 5참고)변환(conversion) 은 하지만 효율성의 측변에서 본다면, 임시 string 객체의 생성과 파괴는 불필요한 비용이다. 그것을 제거하는 것에는 두가지의 일반적인 방법이 있는데, 하나는 당신의 코드에서 해당 변환을 없애 버리는 것이다. 그 방법은 Item 5에 기술되어 있다. 또 하나의 방법은 당신의 소프트웨어를 수정해서 변환 자체가 필요없게 만드는 방법이다. 이것은 Item 21에 방법이 기술되어 있다.
이러한 변환들(conversions)은 오직 객체들이 값으로(by value)나 상수 참조(reference-to-const)로 전달될때 일어난다. 상수 참조가 아닌 참조에서는(reference-to-non-const) 발생하지 않는 문제이다. 다음과 같은 함수에 관하여 생각해 보자:
~cpp void uppercasify(string& str); // str의 모든 글자를 대문자료 바꾼다.글자 세기(charter-counting)예제에서 char 배열은 성공적으로 countChar로 전달된다. 그렇지만 이것과 같은 함수에서는 에러가 발생한다. 다음과 같은 코드 말이다.
~cpp char subtleBookPlug[] = "Effective C++"; uppercasify(subtleBookPlug); // 에러다!어떠한 임시인자(temporary)도 만들어 지지 않았다. 왜 만들어 지지 않은 걸까?
임시인자(temporary)가 만들어 졌다고 가정해 보자. 임시인자는 uppercasify로 전달되고 해당 함수내에서 대문자 변환 과정을 거친다. 하지만 활성화된, 필요한 자료가 들어있는 부분-subtleBookPlug-에는 정작 영향을 끼치지 못한다.;오직 subtleBookPulg에서 만들어진 임시 객체인 string 객체만이 바뀌었던 것이다. 물론 이것은 프로그래머가 의도했던 봐도 아니다. 프로그래머의 의도는 subtleBookPlug가 uppercasify에 영향을 받기를 원하고, 프로그래머는 subtleBookPlug가 수정되기를 바랬던 것이다. 상수 객체의 참조가 아닌 것(reference-to-non-const)에 대한 암시적(implicit) 형변환은 프로그래머가 임시가 아닌 객체들에 대한 변화를 예측할때 임시 객체만을 변경 시킨다. 그것이 언어상에서 non-const reference 인자들을 위하여 임시물들(temporaries)의 생성을 막는 이유이다. Reference-to-const 인자는 그런 문제에 대한 걱정이 없다. 왜냐하면 그런 인자들은 const의 원리에 따라 변화되지 않기 때문이다.
임시객체가 만들어지는 두번째의 경우 그것은 바로 함수에서 반환되는 인자들이다. 예를 들자면 operator+는 반드시 해당 인자들의 합을 표현하는 객체를 반환해야만 한다. 예를들어서 형이 Number로 주어졌다고 했을때 operator+는 다음과 같이 선언된다.
~cpp const Number operator+(const Number& lhs, const Number &rhs);해당 함수의 반환 인자(const Number)는 임시물(temporary)이다. 왜냐하면 그것은 아무런 이름도 가지기 않기 때문이다.:단지 함수의 반환되는 값일 뿐이다. 당신은 반드시 operator+가 호출될때 해당 객체의 생성, 삭제에 관한 비용을 지불해야만 한다. (반환 값이 const인것에 관한 이유를 알고 싶다면 Item 6을 참고하라)
보통 당신은 이러한 비용으로 피해 입는걸 원하지 않는다. 이런 특별난 함수에 대하여 당신은 아마 비슷한 함수들로 교체해서 비용 지불을 피할수 있다.;Item 22는 당신에게 이러한 변환에 대하여 말해 준다. 하지만 객체를 반환하는 대부분의 함수들은 이렇게 다른 함수로의 변환을 통해서 생성, 삭제에 대한 비용 지출에 문제를 해결할 방법이 없다. 최소한 그것은 개념적으로 피할려고 하는 방법도 존재 하지 않는다. 하지만 개념과 실제(concep, reality)는 최적화(optimization)이라 불리는 어두 컴컴한 애매한 부분이다. 그리고 때로 당신은 당신의 컴파일러에게 임시 객체의 존재를 허용하는 방법으로 당신의 객체를-반환하는 함수들수 있다. 이러한 최적화들은 return value oprimization으로 Item 20의 주제이다.
임시 객체의 밑바탕의 생각은 비용이 발생하다. 이다. 그래서 당신은 가능한한 그것을 없애기를 원할 것이다. 하지만 이보다 더 중요한 것은, 이러한 임시 객체의 발생에 부분에 대한 통찰력을 기르는 것이다. reference-to-const(상수참조)를 사용한때는 임시 객체가 인자로 전달될수 있는 가능성이 존재 하는 것이다. 그리고 객체로 함수 값을 반환 할때는 임시 인자(temporary)는 아마도 만들어질것이다.(그리고 파괴되어 진다.) 그러한 생성에 대해 예측하고, 당신이 "behind the scences"(보이지 않는 부분) 에서의 컴파일러가 지불하는 비용에 대한 눈을 배워야 한다.
1.5. Item 20: Facilitate the return value optimization ¶
- Item 20: 반환되는 값을 최적화 하라
유리수를 위한 operator* 를 생각해 보자
~cpp class Rational { public: Rational(int numerator = 0, int denominator = 1); ... int numerator() const; int denominator() const; }; // 반환 값이 왜 const 인지는 Item 6을 참고하라 const Rational operator* (const Rational& lhs, const Rational& rhs);operator*를 위한 코드를 제외하고, 우리는 반드시 객체를 반환해야 한다는걸 알고 있다. 왜냐하면 유리수가 두개의 임의의 숫자로 표현되기 때문이다. 이것들은 임의의 숫자이다. 어떻게 operator*가 그것들의 곱을 수행하기위한 새로운 객체의 생성을 피할수 있을까? 할수 없다. 그래서 새로운 객체를 만들고 그것을 반환한다. 그럼에도 불구하고, C++프로그래머는 값으로 반환시(by-value)시 일어나는 비용의 제거를 위하여 Herculean 의 노력으로 시간을 소비한다.
때로 사람들은 우습게도 이러한 문법으로 포인터를 반환한다.
~cpp // 이러한 객체의 반환은 피해야할 방법이다. const Rational* operator* (const Rational& lhs, const Rational& rhs); const Rational a = 10; Rational b(1,2); Rational c = *(a * b); // 이것이 "자연스러운" 것일까?그것은 또한 의문을 자아 낸다. 호출자가 함수에 의해 반환된 포인터를 바탕으로 객체를 제거 해야 하는거? 이러한 대답에 관해서 보통은 "네" 이고 보통은 자원이 새어나가는(Resource leak)것을 발생 시킨다.
다른 개발자들은 참조(reference)로 반환한다. 물론, 그것은 문법적으로는 수용할수 있다.
~cpp // 객체의 반환을 피하기에는 위험하고 옳바르지 않은 방법이다. const Rational& operator* (const Rational& lhs, const Rational& rhs); const Rational a = 10; Rational b(1,2); Rational c = a * b; // 보기에는 완전해 보인다.그렇지만 이러한 함수는 결코 정확한 행동을 위해서 코드에 적용하지 못한다. 보통 이렇게 짜여 질텐데,
~cpp // 객체를 반환하는 또 다른 멍청하고 위험한 방법이다. const Rational& operator*(const Rational& lhs, const Rational& rhs) { Rational result(lhs.numerator() * rhs.numerator(), lhs.denominator(), rhs.denominator()); return result; }이 함수는 더이상 존재하지 않는 참조를 반환하는 함수이다. 그것이 반환하는 객체는 지역 객체인 result이며 이 result는 함수인 operator*가 종료되면 자동으로 삭제된다. 그래서 반환된 파괴되어진 해당 객체의 참조 값은 쓸수 없다.
이것에 관해서 나를 믿어라:몇몇 함수들(operator*가 그중이다.)는 결국 객체를 반환해야 한다는 것. 그것이 방법이고 이것에 대하여 왈가왈부 하지마라 당신은 이길수 없다.
당신이 값으로(by-value)의 전달을 안쓰고 다른 방법으로 하려는 시도는 결코 이길수 없다. 하지만 이런 잘못된 싸움에서도 얻는것은 있다. 효율의 관점에서 본다면, 당신은 객체를 반환하는 함수에 관해 신경쓰지 말아야 한다, 당신은 오직 객체의 비용에 관해서만 신경 써야 한다. 당신의 노력을 반환 객체에 대한 비용을 줄이는데만 신경 쓰고, 그것들의 제거에 관해서는 신경을 끊기 위해서는 무엇이 필요할까? 만약 이러한 객체들에 관해서 아무런 비용이 발생하지 않는다.면 누가 이런 갯수에 관해서 신경 쓸까?
컴파일러가 임시 인자들에 대한 비용 을 제거할수 있는 그런 방법이라면, 객체를 반환하는 함수 작성에 대해서는 가능할 것이다. 만약 객체 대신에 생성자 구문을 써넣어버리는 트릭(trick)이라면 말이다. 그리고 다음과 같이 할수 있다.
~cpp // 객체를 반환하는 함수에 관하여 효율적이고 정확한 적용을 위해 다음과 같이 한다. const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }반환되는 표현식을 자세히 살펴 봐라. 그것은 Raional의 생성자이다. 당신은 임시 객체 Rational를 다음 표현에서 만들어 내는 것이다.
~cpp Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());그리고 이 임시 객체는 함수가 반환 값을 위하여 복사한다.
이러한 지역 객체 대신의 생성자 구문을 반환하는 작업은 당신에게 더 많은걸 요구한다. 왜냐하면 당신은 아직 임시 객체에 대한 생성과 파괴에 대한 비용을 가지고 있고, 당신은 함수의 반환 객체들의 생성, 파괴에 대한 비용을 지불해야 하기 때문이다. 그렇지만 당신은 몇가지를 얻을수 있다. C++의 규칙은 컴파일러들이 임시 객체를 최적화 시키도록 한다. 결론적으로, 만약 당신이 operator*의 구문이 이것고 같다면,
~cpp Rational a = 10; Rational b(1,2); Rational c = a * b;당신의 컴파일러는 operator*내부의 임시 인자를 없애고 그 임시 인자는 operator*에 의하여 반환 된다. 그들은 객체 c를 메모리에 할당하는 코드에 대하여 return 표현에 의해 정의된 객체를 생성한다. 만약 당신의 컴파일러가 이렇게 한다면 operator*에 대한 임시 객체의 총 비용은 zero가 된다. 게다가 당신은 이것보다 더 좋은 어떠한것을 생각할수 없을꺼다. 왜냐하냐면 c가 이름 지어진 객체이고, 이름 지어진 객체는 사라지지 않기 때문이다.(Item 22참고). 거기에 당신은 inline함수의 선언으로 operator*를 부르는 부하 까지 없앨수 있다.
~cpp inline const Rational operator* (const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }"내~ 내" 하고서 당신은 궁시렁 거릴꺼다. "최적화라..바보 짓이지. 누가 컴파일러가 그렇게 할수 있다고 하는거지? 나는 정말 컴파일러가 저렇게 하는지 알고 싶은데. 진짜 컴파일러가 저런 일을 하는거 맞아?" 이렇게 말이다. 이러한 특별한 최적화-함수의 반환 값을 가능한한 지역 임시 객체가 사용되는 것을 제거해 버리는것-는 유명한 것이고, 일반적으로 구현되어 있다. 그것은 이렇게 이름 붙여진다.:thr return value optimization. 사실 이런 최적화에 대한 이름은 아마 많은 곳에서 설명되어 질꺼다. 프로그래머는 C++컴파일러가 "return value optimization"을 할수 있는지 벤더들에게 물어 볼수도 있을 정도다 만약 한 벤더가 "예"라고 하고 다른 곳은 "뭐요?" 라고 묻는다면 첫번째 벤더는 당근 경쟁력에서 앞서 가는거다. 아~ 자본주의 인가. 때론 당신은 그걸 좋아 할꺼다.
1.6. Item 21: Overload to avoid implicit type conversions. ¶
- Item 21: 암시적(implicit) 형변환의 overload를 피하라
~cpp class UPInt { public: UPInt(); UPInt(int value); ... }; const UPInt operator+( const UPInt& lhs, const UPInt& rhs); UPInt upi1, upi2; ... UPInt upi3 = upi1 + upi2;
여기 관련 구분이다.
이런 구문 역시 성공한다. 그것들은 정수 10을 UPInts로 임시객체가 생성으로 일련의 과정을 수행할수 있다.(Item 19참고)
~cpp upi3 = upi1 + 10; upi3 = 10 + upi2;
컴파일러가 수행해주는 이런 종류의 변환은 참 편하다. 그렇지만 이런 변환에 의해 생성되는 임시 객체는 우리가 바라지 예상하지 않은 비용을 발생시킨다. 많은 사람들은 단지, 이러한 작업에 대하여 아무런 비용을 지불하지 않기를 바라는것 처럼 대다수 C++ 프로그래머들도 암시적 형변환이 비용에 아무런 영향을 끼치지 않기를 원한다. 그렇지만 이런 계산없이 과연 어떻게 수행할수 있을까?
한걸음 뒤로 물러서서, 우리의 목표는 형변환 이 아닌 operator+를 UPInt와 int구분의 혼합으로 호출할수 있게 만들수 있음을 알수 있다. 암시적 형변환의 문제가 끝났것 같다. 그러면, 혼란스런 의미에 종지부를 찍어 보자. 여기 operator+의 수행을 성공시키는 또 다른 혼합된(mixed-type) 호출 방식이 있다. 그것은 처음 시도한 방법에서 암시적 형변환을 제거해 줄것이다. 만약 우리가 UPInt와 int를 합을 할수 있기를 원한다면 우리는 그걸 전부다 그렇게 그대로 만든다. 몇개의 함수를 각기 다른 인자 형(parameter type)으로 overload해서 선언해 버리는 것이다.
우리는 형변환을 제거하기 위하여 overload를 하였다. 하지만 다음과 같은 경우 문제가 발생할수 있다
자 이제 잘 생각해 보자. UPInt와 int의 형을 위해서 우리는 모든 가능한 인자들을 operator+에 구현하기를 원한다. 저 위의 예제의 세가지의 overloading은 잘 수행되지만 양 인자가 모두 int인 경우에는 되지 않느다. 우리는 이것마져 수행할수 있기를 바란다.
~cpp const UPInt operator+(const UPInt& lhs, const UPInt& rhs); const UPInt operator+(const UPInt& lhs, const int rhs); const UPInt operator+(const int lhs, const UPInt& rhs); UPInt upi1, upi2; ... UPInt upi3 = upi1 + upi2; upi3 = upi1 + 10; upi3 = 10 + upi1;
~cpp const UPInt operator+(const int lhs, const int rhs);
이성적이거나, 아니거나, 이 C++ game의 법칙에서 모든 overload한 operator는 반드시 사용자 정의 형(user-defined type)을 가지고 있어야 한다. 그래서 우리는 위와 같은 구문으로 overload할수 없는 것이다.(만약 이런 규칙이 존재하지 않는다면, 프로그래머는 미리 정의된 확실한 의미들의 정의를 해칠수 있다. 예를 들어 위와 같은 int둘에 대해 operator+를 overload가 가능하다면 int의 합에대한 기본 의미가 변화할것이다. 그것을 정말 원하는 사람이 있을까?)
임시객체의 사용을 피하기 위한 operator 함수에 대한 overloading은 특별히 제한되는 것은 없다. 예를들어서 많은 프로그램에서 당신은 string객체가 char*를 수용하기를 바랄것이다. 혹은 그 반대의 경우에도 마찬가지이다. 비슷하게 만약 당신이 complex(Item 35참고)와 같은 수치 계산을 위한 객체를 사용할때 당신은 int와 double같은 타입들이 수치 연산 객체의 어느 곳에서나 유용히 쓰기를 원할 것이다. 결과적으로 string, char*, complex etc 이러한 타입들을 사용하는데 임시 인자의 제거 할려면 모두 overload된 함수가 지원되어야 한다는 것이다.
아직, 80-20 규칙(Item 16참고)은 마음속에 중요하게 남아있겠지. 만약 당신이 그러ㅎ것들을 프로그램에 이용했을때 눈에띠는 성능 향상을 보이지 않는 좋은 생각을 가지고 있다면, overload된 한수들의 제거에 대한 이야기는 결코 논의의 촛점이 되지 않을 꺼다.
1.7. Item 22: Consider using op= instead of stand-alone op. ¶
- Item 22: 혼자 쓰이는 op(+,-,/) 대신에 op=(+=,-=,/=)을 쓰는걸 생각해 봐라.
~cpp x = x + y; x = x - y;
~cpp x += y; x -= y;
좋은 방법은 각 operator와 stand-alone version간에 자연스러운 관계를 생각해서 구현해 주는 것이다. 이것은 위운 예이다.
이 예제는 operator+=과 operator-=이 다른곳에 구현되어 있는 것이고, operator+와 operator-가 이들을 이용해 각기 기능을 구현한 모습이다. 이런 디자인이라면, 이 operator들에게 할당된 기능은 유지될것이다.(다른 것이 변하면 같이 변한다는 소리) 게다가 public 인터페이스에서 operator들이 할당된 버전에서 클래스 상에서 friend로서 stand-alone operator에 대한 필요성은 없다.
~cpp class Rational{ public: ... Rational& operator+=(const Rational& rhs); Rational& operator-=(const Rational& rhs); }; // 값으로의 반환의 이유는 Item 6참고, const의 이유는 p109참고 const Rational operator+(const Rational& lhs, const Rational& rhs) { return Rational(lhs) += rhs; } const Rational operator-(const Rational& lhs, const Rational& rhs) { return Rational(lhs) -= rhs; }
만약 당신이 모든 전역 공간안에 있는 stand-alone operator들에 관하여 마음을 놓치 못한다면, 다음과 같은 template을 사용해서 stand-alone 함수들의 사용을 제거할수 있다.:
다음과 같은 템플릿의 사용으로 operator 할당 버전이 어떠한 형 T로 정의되어져 있는 상태라면 stand-alone operator는 아마도 자동적으로 그것을 생성하게 될것이다.
~cpp template<class T> const T operator+(const T& lhs, const T&rhs) { return T(lhs) += rhs; } template<class T> const T opreator-(const T& lhs, const T& rhs) { return T(lhs) -= rhs; } ...
이런것은 참 좋은 방법이다. 하지만 우리는 효율에 관점에서 생각해 본다면 실패했음을 알수 있다. 이 챕터의 주제는 효율이다. 세가지의 효율의 관점에서 충분히 이런것들은 좋지 못하다.첫번째로 보통 operator할당 버전은 stnad-alone 버전에 비하여 효율이 좋다. 왜냐하면 stand-alone 버전은 반드시 새로운 객체를 반환하고, 그것은 우리에게 임시 객체의 생성과 파괴의 비용을 야기한다.(Item 19, 20참고) operator 할당 버전은 그들의 왼쪽 인자를 기록한다 그래서 해당 operator의 객체 반환시에 아무런 임시 인자를 생성할 필요가 없다.
두번째로 중요한 점은 operator 할당 버전은 stand-alone 버전 만큼 제공하는 것은, 당신의 클래스를 사용하는 클라이언트들에게 효율과 편이성 이 두마리 토끼간을 조율하기가 두 버전다 어렵다는 점이다. 그런 것으로 당신의 클라이언트는 그들의 코드를 다음중 어느거 같이 작성할지 결정할수 있다.
이거나 이렇게 같이
전자의 경우 쓰기 쉽고, 디버그 하기도, 유지 보수하기도 쉽다. 그리고 80%정도의(Item 16참고) 납득할만한 효율을 가지고 있다. 후자는 좀더 전자보다 효율적이고 어셈블러 프로그래머들에게 좀더 직관적이라고 생각된다. 두 버전을 이용한 코드를 모두 제공하여서 당신은 디버그 코드시에는 좀더 읽기 쉬운 stand-alone operator를 사용하도록 할수 있고, 차후 효율을 중시하는 경우에 다른 버전으로(more efficient assignmen) 릴리즈(Release)를 할수 있게 할수 있다. 게다가 stand-alone을 적용시키는 버전으로서 당신은 클라이언트가 두 버전을 바꿀때 문법적으로 아무런 차이가 없도록 확신 시켜야 한다.
~cpp Rational a, b, c, d, result; ... result = a + b + c + d; // 아마 이 연산에서는 각 operaotr+를 호출할때 발생하는 // 3개의 임시 객체가 존재할 것이다.
~cpp result = a; // 하지만 여기에서는 모두 임시 객체가 필요 없다. result += b; result += c; result += d;
마지막으로 효율면에서의 관점으로 stand-alone operator의 적용을 생각해 보자. 다음의 operator+를 위한 코드를 보자:
T(lhs) 라는 표현은 T의 복사 생성자를 호출한다. 이것은 lhs와 동일한 값을 가지는 임시 객체를 생성한다. 이 임시 객체는 operator+=의 rhs로서 쓰이고, 그것은 operator+가 반환하는 결과 값이 된다. 이 코드는 필요없는 비밀(cryptic)로 보인다. 이것 처럼 하는 것이 더 낳지 않을까?:
이런 템플릿은 거의 위의 코드와 동일해 보이지만 결정적인 차이점이 있다. 바로 두번째의 탬플릿은 result인 이름을 가지는 객체를 포함한다는 것이다. 이런 이름 있는 객체가 있다는 사실은 최근에 구현된 반환 값 최적화(return value optimization, Item 20참고)가 operator+에 대하여 작동할수 없다는 것을 의미한다. 두가지중 첫번째 것은 항상 반환 값 최적화(return value optimization)을 수행하기에 적당하다 그래서 당신이 사용하는 컴파일러가 더 좋은 코드를 만들어낼 가능성이 존재 한다.
~cpp template<T> const T operator+(const T& lhs, const T&rhs) { return T(lhs) += rhs; }
~cpp template<class T> const T operator+(const T& lhs, const T& rhs) { T result(lhs); return result += rhs; }
자, 이러한 사실들은 다음과 같은 코드에 관하여 주목하게 하는데,
이런 코드는, 대다수의 컴파일러들이 반환 값 최적화를 수행하는 것 보다 처리하기 더 까다롭다. 처음에 우리는 함수 안에서 단지 당신이 이름 지어진 result을 사용하기 위해 객체에 대한 비용을 지불해야 된다. 이름 지어지지 않은(없는) 객체가 이름 지어진 객체들 보다 더 제거하기 쉬운것은 사실이다. 그래서 이름 지어진 객체와 이름 지어지 않은(없는 ) 객체를 선택하 상황이 주어진다면 당신은 아마 임시 인자들을 사용을 선택하는 것이 더 좋은 방법이다. 그것은 특별히 오래된 컴파일러가 아닌 이상은 결코 이름 지어진 녀석들보다 더 많은 비용을 지불하지 않는다.
~cpp return T(lhs) += rhs;
이름이 존재, 비존재 객체와 컴파일러의 최적화에 다한 이야기는 참 흥미롭다. 하지만 커다란 주제를 잊지 말자. 그 커다란 주제는 operator할당 버전과(assignment version of operator, operator+= 따위)이 stand-alone operator적용 버전보다 더 효율적이라는 점이다. 라이브러리 설계자인 당신이 두가지를 모두 제공하고, 어플리케이션 개발자인 당신은 최고의 성능을 위하여 stand-alone operator적용 버전 보다 operator할당 버전(assignment version of operator)의 사용에 대하여 생각해야 할것이다.
1.8. Item 23: Consider alternative libraries. ¶
- Item 23: 라이브러리 교체에 관해서 생각해 봐라.
서로 다른 디자이너들이 서로 다른 이념(우선순위)에 따라서 이러한 기준(criteria,단수:criterion - 기준)을 설정한다. 그래서 그들은 그들의 디자인에서 다른 어떤 것들을 희생한다. 결과적으로 두개의 라이브러리는 비슷한 기능을 제공하지만 완전히 다른 성능을 보인다.
예를들어서 iostream과 stdio 라이브러리를 생각해 보자. 둘다 모두 C++프로그래머에게 유효하다. iostream 라이브러리는 C에 비해 유리한 몇가지의 이점이 있다. 예를들어서 iostream 라이브러리는 형 안정적이고(type-safe), 더 확장성 있다. 그렇지만 효율의 관점에서라면 iostream라이브러리는 stdio와 반대로 보통 더 떨어진다. 왜냐하면 stdio는 일반적으로 oostream 보다 더 작고 더 빠르게 실행되는 결과물을 산출해 내기 때문이다.
속도를 첫번째 초점으로 삼아 보자. iostream과 stdio의 속도를 느낄수 있는 한가지 방법은 각기 두라이브러리를 사용한 어플리케이션의 벤치마크를 해보는 것이다. 자 여기에서 벤치마크에 거짓이 없는 것이 중요하다. 프로그램과 라이브러리 사용에 있어서 만약 당신이 일반적인 방법으로 사용으로 입력하는 자료(a set of inputs)들을 어렵게 만들지 말아야 하고, 더불이 벤치 마크는 당신과 클라이언트들이 모두 얼마나 일반적 인가에 대한 신뢰할 방법을 가지고 있지 않다면 이것들은 무용 지물이 된다. 그럼에도 불구하고 벤치 마크는 각기 다른 문제의 접근 방식으로 상반되는 결과를 이끌수 있다. 그래서 벤치마크를 완전히 신뢰하는 것은 멍청한 생각이지만, 이런것들의 완전히 무시하는 것도 멍청한 생각이다.
자, 간단한 생각으로 벤치마크 프로그램을 작성해 보다. 프로그램은 가장 근본적인 I/O 기능들만을 수행한다. 이 프로그램은 기본적인 입력과 출력에서 30,000 의 실수를 읽어 나간다. iostream과 stdio간의 선택은 선언된 심볼(symbol) STDIO의 유무에 따라 결정된다. 이 심볼이 선언되어 있으면 stdio 라이브러리가 쓰인 것이고 아니라면 iostream라이브러리가 쓰인 것이다.
이런 프로그램은 양의 정수의 입력의 자연 로그 값을 주는데, 다음과 같은 결과를 보인다.
이렇게 출력을 꾸며놓은 것은 특별한 작업 없이, iostream을 이용해서 고정된 출력 양식을 해놓은 것이다. 물론
같은 여기 이것이 같이 말이다.
그렇지만 operator<<는 형안정(type-safe)이고 확장성(extensible)하다 그리고 printf는 그렇지 못하다.
~cpp #ifdef STDIO #include <stdio.h> #else #include <iostream> #include <iomanip> using namespace std; #endif const int VALUES = 30000; int main() { double d; for (int n = 1; n < VALUES; ++n){ #ifdef STDIO scanf("%lf", &d); printf("%10.5f", d); #else cin >> d; cout << setw(10) // 표현을 10 필드로 고정 << setprecision(5) // 소수점 자리수 결정 << setiosflags(ios::showpoint) // 점 보이도록 << setiosflags(ios::fixed) // 10자리 고정으로 부족한 자리 경우 빈자리 << d; #endif if (n % 5 == 0){ #ifdef STDIO printf("\n"; #else cout << '\n'; #endif } return 0; }
~cpp 0.00000 0.69315 1.09861 1.38629 1.60944 1.79176 1.94591 2.07944 2.19722 2.30259 2.77259 2.48491 2.56495 2.63906 2.70805 2.77289 2.83321 2.89037 2.94444 2.99573 3.04452 3.09104 3.13549 3.17805 3.21888
~cpp cout << setw(10) << setprecision(5) << setiosflags(ios::showpoint) << setiosflags(ios::fixed) << d;
~cpp printf("%10.5f", d);
나는 이 프로그램을 몇가지의 기계와 OS를 조합으로 돌렸다. 그리고 모든 경우의 컴파일러에서 stdio버전이 더 빨랐다. 때로 그것은 약간에서(20%정도) 굉장한 차이(거의200%)까지도 보였다. 하지만 나는 결코 iostream의 적용이 stdio적용 보다 빠른 경우는 찾을수가 없었다. 덧붙여서 프로그램의 크기도 stdio가 더 작은 경향을(때로는 아주 많이 차이가 난다.) 보여준다.(실체 프로그램에서 이러한 차이는 정말 중요하다.)
stdio의 효율의 이점은 기계에 종속적(implementation-dependent)이라는 면을 생각해 보자. 그래서 내가 테스트할 미래의 시스템 들이나, 내가 테스트한 최근의 시스템들은 거의 무시해도 좋을 만큼 iostream과 stdio간의 작은 성능 차이를 보인다. 사실 어떤 부분에서 이론적으로 iostream의 적용이 stdio보다 더 빠른것을 바랄수, 찾을수 있다. 왜냐하면 iostream은 그들의 operand(인자)들을 컴파일 타임에 형을 확정하고 stdio함수들은 일반적으로 실행시간에 문자열로서 해석하기 때문이다.
iostream과 stdio의 이런 상반되는 성능은 단지 예로 촛점은 아니다. 촛점은 비슷한 기능을 제공하는 라이브러리들이 성능과 다른 인자들과의 교환(trade-off)이 이루어 진는 점이다. 그래서 당신은 당신의 프로그램에서 병목현상(bottleneck)을 보일때, 병목 현상을 라이브러리 교체로 해결할 수 있는 경우를 볼수 있다는 점이다. 예를들어서 만약 당신의 프로그램이 I/O병목을 가지고 있다면 당신은 아마 iostream을 stdio로의 교체를 생각해 볼수 있다. 하지만 그것의 시간이 동적 메모리 할당과 해제의 부분이라면 다른 operator new와 opreator delete(Item 8참고)의 교체로 방안을 생각할수 있다. 서로 다른 라이브러리들은 효율성, 확장성, 이동성(이식성), 형안정성, 그리고 다른 주제을 감안해서 서로 다른 디자인의 목적으로 만들어 졌기 때문에 당신은 라이브러리 교체로 눈에 띠게 성능향상의 차이를 느낄수 있을 것이다.
1.9. Item 24: Understand the costs of virtual functions, multiple ingeritance, virtual base classes, and RTTI ¶
- Item 24: 가상 함수, 다중 상속, 가상 기초 클래스, RTTI(실시간 형 검사)에 대한 비용을 이해하라.
1.9.1. Virtual Function ¶
가상 함수가 호출될때 해당 코드는 함수가 불리는 객체에서 동적 형(dynamic type)으로 반응해서 실행된다.;포인터의 형(type)이나 객체의 참조는 중요하지 않다. 어떻게 컴파일러는 이러한 행동을 효율적으로 제공할수 있을까? 대다수의 적용 방법(implementations)은 virtual table(가상 함수 테이블)과 virtual table pointer(가상함수테이블 포인터)를 제공한다. virtual table과 virtual table pointer는 일반적으로 각자 vtbls과 vptrs 로 대신 부를수 있다.
vtbl은 보통 함수 포인터 배열이다. (몇몇 컴파일러는 배열 대신에 연결 리스트(linked-list)를 사용하기도 하지만 근본적으로 같은 기능을 구현한다.) 프로그램 상에서의 그냥 선언되었든, 상속 받았뜬 이런 각각의 클래스는 모두 vtbl을 가지고 있다. 그리고 클래스의 vtbl안에 있는 인자들은 클래스에 대한 가상 함수의 구현 코드에 지칭하는 포인터들의 모임이다. 예를들어서 다음과 같이 클래스가 선언되어 있다면
C1의 virtual table 배열은 아마 다음과 같이 표현될 것이다.
~cpp class C1{ public: C1(); virtual ~C1(); virtual void f1(); virtual int f2(char c) const; virtual void f3(const string&s); void f4() const; ... };
가상 함수가 아닌(nonvirtual function) f4는 테이블에 기술되어 있지 않고, C1의