11. 선택 연산

11.2 힙 영역 메모리

11.2.1 메모리 관리

  • 힙 저장 공간의 주요 문제점은 다음과 같다.

    • 메모리 누수(leaked object) : new 를 통해 힙 영역에 데이터를 생성하고 나서 delete 를 사용하지 않을 때 메모리 누수가 발생한다. -> 메모리 누수는 프로그램의 저장 공간을 줄이기 때문에, 심각한 문제가 될 가능성이 있다.

    • 빠른 소멸(premature deletion) : 개체를 delete 하고 나중에 그 포인터를 사용하고자 할 때

    • 중복 소멸(double deletion) : 개체를 두번 소멸할 때 -> 한번 개체를 소멸하면, 해당 메모리 공간은 운영체제에 의해 다른 변수에게 할당될 수 있다. -> 두번 개체를 소멸하는 것은 다른 변수의 메모리 공간을 소멸한다는 것! (정말 잘못된 상황을 초래할 수 있다.. )

  • 위와 같은 문제점이 있기에, 'new', 'delete' 를 사용하는 방식에 두 가지 권장 사항이 있다.

    • 꼭 필요하지 않다면, 동적 할당을 하지 않는다. -> 스택에 변수를 우선적으로 저장한다.

    • 힙 영역에 개체를 생성할 때, 그것들을 관리해주는 표준 라이브러리 or 컨테이너를 사용해라. -> 예를 들어, STL 컨테이너는 기본적으로 동적으로 메모리 공간을 관리해주고, 내부적으로 개체의 생성과 소멸을 담당한다. -> string, vector, unique_ptr, shared_ptr 등 ..

11.2.2 배열

  • 개체의 배열 역시 new 로 생성할 수 있다.

  • 개체의 배열을 삭제할 때 delete[] 가 사용된다. -> [] 를 사용하지 않는다면 배열의 첫번째 메모리만 해제되고, 나머지 해제되지 않는다.

  • 배열과 컨테이너는 new 를 통해 힙영역에 생성할 수 있지만, 지역 개체에는 new 를 사용하지 말아야 한다. -> 아래 코드는 비효율적이다.. -> return 이나 delete 이전에 발생한 예외는 메모리 누수를 일으킬 것이다. (지역 변수를 사용하자.. )

// 사용하면 안되는 방식!
void f1()
{
    X* p = new X;
    // ... *p 를 사용한다. 
    
    delete p;
}

// 사용해도 되는 방식.
void f2() 
{
    X x; 
    // ... x 를 사용한다. 
}

11.2.3 메모리 공간 확보

  • new 키워드를 통해 힙 영역의 메모리 공간을 확보할 수 있다.

  • new 가 할당할 수 있는 공간을 찾지 못하면 기본 설정 상 std::bad_alloc 이라는 예외를 던진다. -> 아래 코드는 엄청난 메모리 공간을 소모하고, 결국 bad_alloc 예외를 호출할 수 밖에 없다.

11.3 리스트 {}

11.3.1. 리스트 {} 구현 모델

{} 는 아래와 같이 다양한 용도로 사용된다.

-> 이는 {} 가 범용 모델이며 컴파일러에 의해 효율적인 최적화가 가능하다.

  1. {} 가 생성자로 사용되는 경우 -> A a{1, 2}; 의 형태로 사용되는데, 해당 형태는 데이터 손실이 있는(Narrowing) 변환을 불어한다. -> 또한 initializer_list 를 매개변수로 받는 생성자가 오버로딩 되어 있다면 컴파일러는 가장 먼저 해당 생성자를 사용하기 때문에, 사용에 주의가 필요하다.

  2. {} 가 어떤 집합체(배열, 컨테이너, 클래스 등.. ) 의 원소를 초기화하는 경우

  3. {} 가 initializer_list 개체를 생성하는데 사용되는 경우

{} 타입은 auto 와 함께 사용시 모든 원소가 동일한 타입일 경우에만 사용이 가능하다.

11.3.2 qualified list

  • 특정 클래스나 구조체의 멤버를 초기화 할 때 {} 사용하면 qualified list 라고 부른다.

11.3.3 unqualified list

  • 특정 클래스나 구조체의 멤버를 초기화하는 용도가 아닌 경우 unqualified list 라고 부른다.

일반적인 템플릿 인자에 대해 unqualified list 타입을 추론할 수 없다.

  • auto 타입은 {} 를 initializer_list<> 타입으로 추론해 주었지만, 템플릿에서는 명시적으로 타입을 지정해주지 않는다면 컴파일 에러가 발생한다. -> 이는 언어상의 제약이다!

  • 마찬가지로 아래의 경우에도 컴파일러는 타입을 추론하지 못해 컴파일 에러를 발생시킨다. -> f2 함수 사용 어디에도 vector 를 사용한다는 걸 알 수 없다.. (추론 불가) -> 너무 모호한 정보만을 제공하고 추론해달라고 하는 것은 약간 무책임? 하다 생각된다..

11.4 람다 표현식

  • 람다는 이름이 없는 함수 개체를 정의하고, 사용하기 위한 목적의 단순화된 표기법이다.

  • 이런 사용 방식은 일반적인 함수 사용 방식보다는, 특히 연산을 인자로 알고리즘에 전달하고자 할 때 유용하다!

  • 람다 표현식은 몇가지 부분으로 나뉜다.

    • 캡처 리스트 : 캡부 변수들을 복사 or 참조할 것인지를 표현

    • 매개변수 리스트 : 람다 표현식에 어떤 인자가 필요한지 표현

    • mutable 지정자 : 값에 의해 갭처된 변수의 대한 람다의 복제본을 변경 하도록 표현

    • -> 타입 : 반환타입을 지정

    • 람다 본체 {} : 실행될 코드를 지정

11.4.1 구현 모델

실제로 람다식은 컴파일러가 해당 람다식에 대한 익명 클래스를 생성하고, 해당 클래스의 개체를 만들어서 사용한다!

  • 람다로 구현한 다음 코드를 살펴보자

  • 위 코드의 의미를 살피기 위해서 컴파일러가 만들어낸, 동일한 역할을 하는 함수가 있는 개체를 정의해보자.

    • 캡처 리스트는 두 개의 멤버 변수와 그것을 초기화하는 하나의 생성자가 된다. (복사생성자)

    • 람다의 본체는 단순히 operator() 의 본체가 된다.

    • 위 람다식은 값을 반환하지 않기 떄문에, 리턴타입은 void 이다.

    • 기본 설정상 operator() 는 const 이므로, 람다 본체는 캡처된 변수를 변경하지 않는다. -> 람다 본체에서 캡처된 변수를 변경하고 싶다면 mutable 로 선언해야 한다.

    • 아래와 같이 람다로부터 생성되는 임시 클래스 개체를 closure object 라고 부른다.

클로저 개체(closure object)

  • 람다 표현식이 사용될 때 컴파일러는 클로저 객체라는 특별한 타입의 객체를 생성합니다.

  • 클로저 개체는 다음과 같은 특징을 가진다.

    • 캡처된 변수들

      • 람다 표현식의 캡처 목록에 있는 변수들을 멤버 변수로 저장합니다.

      • 캡처 방법에 따라 값으로 캡처하거나 참조로 캡처할 수 있습니다.

    • 연산자 오버로딩

      • 람다 표현식의 함수 본문은 클로저 객체의 operator()로 구현됩니다.

  • 아래의 람다식을 컴파일러가 클로저 개체로 만들어 준다면 대략적으로 다음과 같을 것이다.

  • 의미를 파악했으니 실제 코드를 사용할 때 아래와 같이 사용할 수 있다. -> 재사용 할 것 같지 않는 코드인데, 내부적으로 클래스를 정의하는 것은 생각보다 번잡한 일이다..

11.4.2 람다의 대안

  • 람다를 사용하기 이전에 케이스부터 람다를 사용하고 또 개선하는 케이스를 살펴보자.

step1. 람다식 대신 단 한번만 사용되는 직전 클래스 생성

step2. 람다식 사용

step3. for_each 대신 for 문사용(람다식 미사용)

-> 더욱 직관적이다.

step4. 좀 더 많은 컨테이너를 사용하기 위한 템플릿 사용

11.4.3 캡처

11.4.3.1 람다와 수명

  • 람다는 참조를 캡처할 때, 호출자보다 오래 살아남을 수 있다. -> 이 때문에, 문제가 생기게 된다.

  • 아래 코드가 문제가 생기는 과정은 다음과 같다.

    • setup() 이 종료되고 차후에(아마 몇분 뒤) 사용자가 "draw triangle" 버튼을 누르면 람다는 오래 전에 사라진 지역 변수에 접근하려 할 것이고,

    • 이는 지역변수가 이미 사라진 후이기 때문에, 문제가 발생한다.

  • 때문에, 람다가 호출자보다 오래 살아 남을것이라고 생각한다면 값들이 return 매커니즘이나, 적절한 인자를 통해서 반환 되도록 만들어야 한다. -> = 캡처 리스트는 람다가 만들어지는 순간 값을 복사해놓기 때문에, 이후에 사용하더라도 문제가 없다.

11.4.3.3 람다와 this

  • 멤버 함수에 쓰인 람다에서 클래스 개체의 멤버에 접근하기 위해서 캡처 리스트에 this 를 추가하면 된다. -> [this] 은 값에 의한 캡처가 아닌, 참조에 의한 캡처이다. -> 때문에, 주의가 필요하다..

11.4.3.4 mutable 람다

  • 대부분의 함수 개체(closure Object) 의 상태를 변경할 필요가 없기 때문에, 기본 설정상 값에 의해 캡처된 값에 대한 변경은 불가능하게 되어있다. -> operator() 는 const 이다.

  • 상태를 변경해야 하는 이벤트가 만약에 생긴다면 mutable 로 선언할 수 있다.

Last updated