추후 내용 보충. 예정

 

std::movestd::forward는 모두 C++11 이후부터 도입된 유틸리티 함수들로, 둘 다 값의 전달 방식을 조절하는 데 사용됩니다. 그러나 각각의 역할과 사용하는 상황이 다르므로, 이 둘을 정확히 이해하는 것이 중요합니다.

std::move

역할: std::move는 주어진 인자를 "이동"할 수 있도록 하는 역할을 합니다. 이동(move)이란 객체의 자원(메모리 등)을 다른 객체로 옮기는 것을 말하며, 복사보다 효율적인 자원 관리를 가능하게 합니다.

사용 방법: std::move는 주로 객체를 이동할 때 사용됩니다. 이 함수는 주어진 인자를 rvalue reference로 변환하여, 이동 연산자(move constructor 또는 move assignment operator)를 호출할 수 있게 합니다.

예시:

#include <utility>
#include <vector>

int main() {
    std::vector<int> v1 = {1, 2, 3};
    std::vector<int> v2 = std::move(v1); // v1의 자원을 v2로 이동

    // 이제 v1은 비어 있음

    return 0;
}

std::forward

역할: std::forward는 주어진 인자를 "전달"할 때 사용됩니다. 전달(forward)이란 함수 템플릿의 매개변수를 다른 함수에 그대로 전달하는 것을 의미합니다. 이는 템플릿 매개변수의 형태를 유지하면서 완벽한 전달(forwarding)을 가능하게 합니다.

사용 방법: std::forward는 주로 forwarding reference (universal reference)를 사용할 때 유용합니다. forwarding reference는 T&& 형태의 매개변수로, 템플릿 매개변수의 형태를 유지하면서 왼값(lvalue)와 오른값(rvalue) 모두를 받을 수 있습니다.

예시:

#include <utility>

// 예시 함수 템플릿
template <typename T>
void wrapper(T&& arg) {
    some_function(std::forward<T>(arg)); // arg를 그대로 전달
}

int main() {
    int x = 42;
    wrapper(x); // 왼값 전달
    wrapper(123); // 오른값 전달

    return 0;
}

차이점 정리

  • std::move는 주로 이동 연산을 활성화시키기 위해 사용되며, 객체의 자원을 다른 객체로 이동시킵니다.
  • std::forward는 주로 forwarding reference를 다룰 때 사용되며, 완벽한 전달(forwarding)을 통해 함수 템플릿 매개변수의 형태를 유지하면서 인자를 전달합니다.

이 두 함수는 모두 템플릿 프로그래밍과 제네릭 프로그래밍에서 매우 중요한 역할을 합니다. 특히 이동 시맨틱과 퍼펙트 포워딩(perfect forwarding)에서 중요한 개념들입니다.

'Programming Language > C++' 카테고리의 다른 글

[C++] 클래스와 함수의 friend 선언  (0) 2024.06.29
[C++] 스마트 포인터  (0) 2024.06.28
[Modern C++] 함수 객체와 람다식  (0) 2024.06.28
[C/C++] 함수 포인터  (0) 2024.06.28
[C++] 예외 처리(Exception handling)  (0) 2024.06.28

friend 선언(클래스, 함수)

  • friend는 친구라는 의미를 가지고 있다.

클래스의 friend 선언


클래스를 friend로 선언한다는 것은 어떤 의미일까?
예를 들어 클래스 A와 B가 있다고 해보자. 이 때 클래스 A가 "클래스 B는 내 믿을만한 친구야."라고 컴파일러에게 알려준다. 그러면 클래스 B에서 클래스 A의 모든 멤버(private까지)를 접근할 수 있게 된다

  • A 클래스가 B 클래스를 friend 클래스로 선언하면, B 클래스는 A 클래스의 private 멤버까지 직접 접근이 가능하다.
    • a.GetX()가 아니라 a.x 이런식으로 직접 접근이 가능해진다
  • 반대로 A 클래스가 B 클래스의 private멤버까지 직접 접근을 하기 위해서는 B 클래스에서 A 클래스를 frined 클래스로 선언해줘야 한다
  • 즉, 클래스의 friend 선언은 private 멤버에 접근할 수 있는 클래스를 지정해주는 것이다
  • 이는 OOP의 정보은닉(Encapsulation)에 위배되는 선언이므로, 가급적 사용하지 않는 것이 좋다.

example

#include <cstring>
#include <iostream>
using namespace std;
class B; // 컴파일러에게 먼저 classB가 있다는 것을 알려줘야 classA에서 classB를 frined로 선언할 수 있다
class A {
   public:
    A(int mynum) : num(mynum) {}

   private:
    int num;
    friend class B;
};

class B {
   public:
    B(int mynum) : num(mynum) {}
    void ShowAclass(A &frn) {
        cout << "A의 private num은 " << frn.num << endl;
    }

   private:
    int num;
};

int main(void) {
    A a(3);
    B b(5);
    b.ShowAclass(a);
}

함수의 friend 선언

  • 클래스와 마찬가지로 함수 또한 friend로 선언할 수 있다
  • 비 멤버함수에서 객체의 private 멤버에 접근할 수 있게 된다
#include <iostream>
using namespace std;

class A {
   public:
    A(int mynum) : num(mynum) {}

   private:
    int num;
    friend void ShowClass(const A&);
};

void ShowClass(const A& aclass) {
    cout << "나의 Private num은: " << aclass.num << endl;
}

int main(void) {
    A a(3);
    ShowClass(a);
}

Ref) https://musket-ade.tistory.com/entry/C-클래스의-friend-선언-함수의-friend-선언

'Programming Language > C++' 카테고리의 다른 글

[Modern C++] std::move, std::forward  (0) 2024.06.30
[C++] 스마트 포인터  (0) 2024.06.28
[Modern C++] 함수 객체와 람다식  (0) 2024.06.28
[C/C++] 함수 포인터  (0) 2024.06.28
[C++] 예외 처리(Exception handling)  (0) 2024.06.28

스마트 포인터

  • 스마트 포인터를 이용하면 동적할당된 메모리를 알아서 지워준다

  • 3가지 종류 존재

    • unique_ptr

    • shared_ptr

    • weak_ptr

unique_ptr

  • int *a = new int(5);를 스마트 포인터를 이용하여 동적할당해보자
#include <iostream>
#include <memory>

using namespace std;

int main() {
        // int *a = new int(5);를 스마트 포인터를 이용하여 생성해보자
    // 다음은 불가능
    // unique_ptr<int> a = new int(5);
    unique_ptr<int> a(new int(5));
}
  • unique_ptr a = new int(5);은 불가능함

    • 이 경우 변환 생성자가 호출되는데, 스마트 포인터의 경우 묵시적 형변환이 불가능하기 때문
  • 따라서 생성자를 이용해야 한다

    • unique_ptr a(new int(5));
  • a가 scope(위 예시에서는 메인함수)를 벗어나면 a가 사라지면서 a가 가리키는 객체가 사라진다

  • 생성자와 소멸자를 통해서 작동 과정을 실제로 확인해보자

#include <iostream>
#include <memory>

using namespace std;

class Test {
    public:
    Test() { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
};

int main() {
    cout << "main 함수 시작" << endl;
    unique_ptr<Test> a(new Test);
    cout << "main 함수 종료" << endl;
}
  • delete를 적어주지 않았는데도 스마트 포인터가 객체를 알아서 삭제해준다

왜 unique일까?

  • 여러 개의 포인터가 같은 객체를 가리킬 수 없게 막아뒀기 때문

    • a, b, c라는 포인터가 있을 때, a, b, c는 같은 객체를 가리킬 수 없다
#include <iostream>
#include <memory>

using namespace std;

class Test {
    public:
    Test() { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
};

int main() {
    cout << "main 함수 시작" << endl;
    unique_ptr<Test> a(new Test);
    /* 다음은 불가능
    // unique_ptr<Test> b(a);
    // b = a
    */
    cout << "main 함수 종료" << endl;
}
  • b라는 포인터가 a가 가리키는 객체를 가리키려고 하고 있기 때문에 오류가 발생한다

    • unique_ptr b(a); 그리고 b = a;는 불가능하다

메모리 소유권

#include <iostream>
#include <memory>

using namespace std;

class Test {
    public:
    Test() { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
};

int main() {
    int *a = new int(5);
    int *b = a; // 얕은 복사

    cout << *b << endl;
    delete a;
    // 한 번 더 해제하면 안된다
    // delete b;
}
  • a와 b 중 객체를 삭제할 의무를 가진게 메모리를 해제해줘야 한다

    • 이런 것을 메모리 소유권이라고 한다

    • 위 예시에서는 a가 객체의 메모리 관리를 전담해서 하고 있다.

    • b는 메모리 소유권을 가지고 있지 않다

int main() {
    int *b;
    if (...) {
        int *a = new int(5);
        /// ...
        b = a; // 소유권 이전
    }
    cout << *b << endl;
    // delete a를 할 수가 없다.
    // 메모리 소유권이 b로 이전되어 b에서 해제해준다
    delete b;
}
  • if문이 끝나면 a 포인터가 사라지기 때문에 5의 값을 갖는 객체를 접근할 수 있는 포인터가 b밖에 없다.

    • 따라서 b를 통해서만 delete를 할 수 있다.
    • b = a;에서 메모리 소유권을 이전해준다
int main() {
    int *a = new int(5);
    int *b = a; // 소유권 이전이라고 "해석"

    cout << *b << endl;
        delete b
}
  • 아까와 동일한 코드. 위에서는 delete a;를 해줬다

  • 여기서는 delete b로 해제하고 있다.

    • 코드는 똑같은데 위에서는 a로 해제하고, 여기서는 b로 해제하고 있다.

    • 동일한 코드인데 메모리 소유권이 다를 수가 있나?

      • 소유권이라는 개념은 의미론적인 개념이다. 문법적으로 소유권이 넘어갔다는 것을 알 수 있는 방법은 없음

      • int *b = a; 에서 소유권 이전이라고 "해석" 할 뿐이다 (semantic. 의미를 해석)

  • 스마트 포인터를 사용하면 소유권을 문법적인 차원에서 명시적으로 드러나게 할 수 있다

int main() {
    unique_ptr<int> a(new int(5));
    // b에게 소유권을 이전하고 싶으면 a가 5를 갖는 객체와 연결을 끊어줘야 한다
    unique_ptr<int> b(a.release()); // 소유권 이전
    // release() : a가 가지고 있는 소유권을 포기하고 다른 포인터로 넘겨주겠다.
    cout << *b << endl;
}
  • unique_ptr이기 때문에 b에게 소유권을 이전하고 싶으면 a가 5를 갖는 객체와 연결을 끊어줘야 한다

    • unique_ptr b(a.release()); 에서 소유권 이전

    • release() : a가 가지고 있는 소유권을 포기하고 다른 포인터로 넘겨주겠다

  • 그리고 소유권 이전을 안하고 참조를 복사할 수 있는 방법은 없다.

    • 소유권 이전 안하고 unique_ptr b(a); 이런 것은 불가능

reset()

int main() {
    unique_ptr<int> a(new int(5));
    a.reset(new int(6));
}
  • reset()은 재설정을 한다는 의미이다.

    • a가 가리키는 객체를 다른 객체로 바꾼다는 것
  • 클래스의 생성자와 소멸자를 통해서 동작을 확인해보자

#include <iostream>
#include <memory>

using namespace std;

class Test {
   public:
    Test(int x) : x(x) { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
    int GetX() const { return x; }

   private:
    int x;
};

int main() {
    unique_ptr<Test> a(new Test(5));
    cout << a->GetX() << endl;
    cout << "====" << endl;
    a.reset(new Test(6));
    cout << a->GetX() << endl;
    cout << "====" << endl;
}
  • 실행 결과
생성자
5
====
생성자
소멸자
6
===
소멸자
  • new Test(6) 객체가 먼저 생성되고 Test(5) 객체가 삭제된다

shared_ptr

  • 여러 포인터가 한 객체를 공유할 수 있다
#include <iostream>
#include <memory>

using namespace std;

class Test {
   public:
    Test(int x) : x(x) { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
    int GetX() const { return x; }

   private:
    int x;
};

int main() {
    // 방식 1
    shared_ptr<Test> a = make_shared<Test>(5);
    // 방식 2
    shared_ptr<Test> a(new Test(5));

    shared_ptr<Test> b = a;

}
  • shared_ptr를 만드는 방식에는 두 가지 방법이 있다.

    • make_shared 이용

      • shared_ptr a = make_shared(5);
    • 생성자 이용

      • shared_ptr a(new Test(5));
  • shared_ptr는 복사 생성, 복사 대입 모두 가능하다

    • shared_ptr b = a;

    • shared_ptr b(a);

    • 여러 포인터가 한 객체를 가리킬 수 있게 된다

  • shared_ptr은 use-count라는 특별한 값을 저장하여, 이 객체를 몇 개의 포인터가 가리키고 있는지 관리한다

    • a, b가 같은 객체 Test(5)를 가리키는 경우 use-count의 값은 2

    • a가 사라진 상황에서는 use-count의 값은 1

    • b가 사라진 상황에서 use-count는 0

  • b.use_count()을 통해서 실제 use-count 값을 확인해볼 수도 있다

int main() {
    shared_ptr<Test> a(new Test(5));
    {
        shared_ptr<Test> b = a;
        cout << a.use_count() << endl;
        cout << b.use_count() << endl;
    }
    cout << a.use_count() << endl;
}   
  • 실행 결과
생성자
2
2
1
소멸자

weak_ptr

  • weak, 약하다: 소유권은 없지만 어떤 객체를 가리키는 것은 할 수 있다.

  • a가 가리키는 Test(5)를 weak 포인터 b가 가리킬 때

    • a가 삭제되면 use_count는 0이 된다
#include <iostream>
#include <memory>
using namespace std;

class Test {
   public:
    Test(int x) : x(x) { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
    int GetX() const { return x; }

   private:
    int x;
};

int main() {
    weak_ptr<Test> b;
    {
        shared_ptr<Test> a(new Test(5));
        b = a;
        cout << a.use_count() << endl;
        cout << b.use_count() << endl;
    }
    cout << b.use_count() << endl;
        // 다음은 불가능
        // cout << b->GetX() << endl;
}   
  • 실행 결과
생성자
1
1
소멸자
0
  • 마지막 줄 cout << b.use_count() << endl;에서

    • b는 이미 소멸된 객체를 가리키고 있다.

    • cout << b->GetX() << endl; 이런 것은 불가능한다

    • b는 가리키는 객체가 소멸된 Dangling pointer 이기 때문이다

  • 대신에 b가 가리키는 객체가 존재하는지 확인할 수 있는 방법이 존재한다

int main() {
    weak_ptr<Test> b;
    {
        shared_ptr<Test> a(new Test(5));
        b = a;
        cout << a.use_count() << endl;
        cout << b.use_count() << endl;
    }
    cout << b.use_count() << endl;
    if (!b.expired()) {
        cout << b.lock()->GetX() << endl;
    }
}   
  • b가 가리키는 객체가 소멸되지 않았다면 b.lock()에서 shared_ptr를 받아와서 GetX()를 호출할 수 있다.

  • 반대로 만약 b가 가리키는 대상이 이미 소멸했다면 b.lock()은 널 포인터를 리턴한다

참조) 두들낙서 C/C++ 강좌

함수 객체와 람다식

  • 함수 객체 : 함수처럼 작동하는 객체 (function object/functor)
  • 예제 코드를 보기 전에 알아야 하는 내용이 있다.
bool f(int x) {
    //
}

int main() {
        f(1);
}
  • f(1)에서의 소괄호 ()는 연산자임을 기억하자.
    • (물론 f는 객체가 아니긴 하지만) f.operator()(1); 이런 식으로 연산자를 호출해서 작동을 한다.
  • 그래서 소괄호 연산자에 대해서도 오버로딩을 해줄 수가 있다.
#include <iostream>
using namespace std;

class Equals {
   public:
    Equals(int value) : value(value) {}
    bool operator()(int x) const {
        return x == value;
    }

   private:
    int value;
};

int main() {
    Equals eq(123);
    cout << eq(12) << endl;
    cout << eq(123) << endl;
}
  • Equals eq(123)에서 소괄호 (123)은 소괄호 연산자가 아니라, 생성자이다.
  • eq(12)는 Equals의 소괄호 연산자()를 호출하라는 의미이다
    • 이 때 매개변수로 12를 받는다
  • 함수 객체의 좋은점 : 같은 함수 객체로 여러가지 함수를 호출할 수 있다.
class Equals {
   public:
    Equals(int value) : value(value) {}
    bool operator()(int x) const {
        return x == value;
    }

        bool operator()(int x, int y) const {
            return x + y == value
        }

   private:
    int value;
};

int main() {
    Equals eq(123);
    cout << eq(12, 111) << endl;
    cout << eq(123) << endl;
}
  • 메인함수에서 같은 객체를 통해서 서로 다른 함수를 호출하고 있다.
  • 함수 포인터랑 다른점 : 변수를 가지고 있다. (상태를 가지고 있다.)
int main() {
    Equals eq(123), eq2(234);
    cout << eq2(123) << endl; // false
    cout << eq2(234) << endl; // true
}

함수 포인터를 함수 객체로 바꾸기

#include <iostream>
using namespace std;

class Func {

};

int square(int x) { return x * x; }
int myFunc(int x) { return x * (x - 15) / 2; }

int arrFnMin(const int arr[], int n, int (*f)(int)) {
    int min = f(arr[0]);
    for (int i = 1; i < n; i++) {
        if (f(arr[i]) < min) {
            min = f(arr[i]);
        }
    }
    return min;
}

int main() {
    int arr[7] = { 3, 1, -4, 1, 5, 9, -2};
    cout << arrFnMin(arr, 7, square) << endl;
    cout << arrFnMin(arr, 7, myFunc) << endl;
}
  • 함수 포인터를 알고 있으니 int를 매개변수로 받아서 int를 리턴해주는 여러 함수를 Func이라는 하나의 클래스로 표현할 수 있을 것 같지만 불가능 하다
    • square()에 해당하는 클래스와 myFunc()에 해당하는 클래스가 따로 있어야 한다
  • 참고 : 아래 두 개 코드는 동일하게 작동한다.
class A {
        public:
        // 생략
} a;
class A {
        public:
        //생략
};

A a;
  • square()와 myFunc()를 함수 객체로 만들어 주었다.
class Square {
   public:
    int operator()(int n) { return n * n; }
} square;  // Square타입의 함수 객체 square 생성

class MyFunc {
   public:
    int operator()(int n) { return n * (n - 15) / 2; }
} myFunc;  // MyFunc타입의 함수 객체 myFunc 생성

// 두 함수를 함수 객체로 만들어준다
// int square(int x) { return x * x; }
// int myFunc(int x) { return x * (x - 15) / 2; }

int arrFnMin(const int arr[], int n, int (*f)(int)) { 
/* 생략 */
  • 그리고 나서 보니, arrFnMin의 int (*f)(int) 자리에 어떤 것을 매개변수로 받아야 할지 고민이 될 것이다.
    • Square를 넣어야 할까? MyFunc를 넣어야 할까?
    • 기존에 알던 내용을 기반으로 생각해보면, Square와 MyFunc의 부모 클래스를 만들어서 오버라이딩 해주면 된다.
    • 다른 방법에 대해서 배워보자
  • functional 라이브러리를 이용해보자
#include <iostream>
#include <functional>
using namespace std;

/* 중략 */

int arrFnMin(const int arr[], int n, function<int(int)> f) {
    int min = f(arr[0]);
    for (int i = 1; i < n; i++) {
        if (f(arr[i]) < min) {
            min = f(arr[i]);
        }
    }
    return min;
}
  • square와 myFunc 모두 int를 매개변수로 받아 int를 반환해주고 있다.
  • function<int(int)> f
    • function<int(int)>인 이유는
      • (int) : int를 매개변수로 받아
      • int를 반환
  • function<int(int)>타입인 f 객체는 int를 매개변수로 받고 int를 반환해주는 객체를 가리킬 수 있게 된다
  • 여기에 치명적인 단점이 있다
    • 성능(Performance)가 안좋아서 function이라는 클래스 템플릿은 성능상 사용하지 않는 것이 좋다
  • 더 나은 해결책을 찾아보자 : template 이용
#include <iostream>
#include <functional>
using namespace std;

class Square {
   public:
    int operator()(int n) { return n * n; }
} square;  // Square타입의 함수 객체 square 생성

class MyFunc {
   public:
    int operator()(int n) { return n * (n - 15) / 2; }
} myFunc;  // MyFunc타입의 함수 객체 myFunc 생성

template<typename T>
int arrFnMin(const int arr[], int n, T f) {
    int min = f(arr[0]);
    for (int i = 1; i < n; i++) {
        if (f(arr[i]) < min) {
            min = f(arr[i]);
        }
    }
    return min;
}

int main() {
    int arr[7] = {3, 1, -4, 1, 5, 9, -2};
    cout << arrFnMin(arr, 7, square) << endl;
    cout << arrFnMin(arr, 7, myFunc) << endl;
}
  • function<int(int)>를 T, 즉 f 자체의 임의의 타입으로 받겠다는 것이다
  • arrFnMin(arr, 7, square)를 호출하면
    • int arrFnMin(const int arr[], int n, T f)에서 T에 square가 들어간 함수가 생성되어 호출될 것이다.
      • f의 타입은 Square가 된다.
      • Square 클래스 안에 소괄호() 연산자가 존재하므로, f(arr[0]) 이런 것들이 가능해진다
  • 장점
    • 클래스 템플릿의 경우 모든 것이 컴파일 타임에 모든게 결정되므로 성능에 대한 걱정을 하지 않아도 된다. 효율적으로 작동할 것이다
    • arrFnMin(arr, 7, square)에서 굳이 세 번째 매개변수에 함수 객체를 넘기지 않고 함수 포인터를 넘길 수도 있다
      • 함수 포인터를 사용하는 경우에도 같은 템플릿을 사용해서 처리할 수 있다.
  • 아직까지 불편한 점이 아직 남아있다
    • 우리가 하고 싶은 것은 제곱(square)과 같이 특정 연산을 하는 함수를 arrFnMin 함수에 넘겨주고 싶었다
    • n을 제곱해주는 함수라는 것을 알려주기 위해서 많은 작업을 하게 된다
      • Square 라는 클래스를 만들고, 소괄호 연산자 오버로딩도 하고... 해야 한다
#include <iostream>
#include <functional>
using namespace std;

int square(int n) {return n * n;}

class MyFunc {
   public:
    int operator()(int n) { return n * (n - 15) / 2; }
} myFunc;  // MyFunc타입의 함수 객체 myFunc 생성

template<typename T>
int arrFnMin(const int arr[], int n, T f) {
    int min = f(arr[0]);
    for (int i = 1; i < n; i++) {
        if (f(arr[i]) < min) {
            min = f(arr[i]);
        }
    }
    return min;
}

int main() {
    int arr[7] = {3, 1, -4, 1, 5, 9, -2};
    cout << arrFnMin(arr, 7, square) << endl;
    cout << arrFnMin(arr, 7, myFunc) << endl;
}
  • 위에서 함수 포인터를 이용한 코드 동작 과정을 보면
    • 먼저 위에서 square 함수가 어떤 함수인지(제곱을 리턴해 주는 함수)를 먼저 알려준 다음
      • "이런 기능을 하는 함수가 있는데, 이름이 square 이다" 라고 선언하고
    • cout << arrFnMin(arr, 7, square)에서 square를 넘겨주게 된다
  • 우리는 arrFnMin(arr, 7, square) 여기에다가 이러한 기능을 하는 함수다라고 바로 알려주고 싶다.
    • 그래서 등장한게 람다식(익명 함수)이다
    • 람다식(익명 함수) : 이름이 없는 함수
      • 여기서는 square라는 이름이 없고, 제곱하는 내용만 들어있는 함수
int main() {
    int arr[7] = {3, 1, -4, 1, 5, 9, -2};
        // 람다식
    cout << arrFnMin(arr, 7, [](int n) -> int {return n * n;}) << endl;
    cout << arrFnMin(arr, 7, myFunc) << endl;
}
  • int 형 매개변수를 n을 받고
    • (int n)
  • int를 반환하는 함수이고
    • -> int
  • 함수의 내용물이 n * n을 리턴하는 함수이다.
    • {return n * n;}
  • 함수의 본문은 있지만, 이름이 없는 구조라는 걸 알 수 있다.
  • myFunc도 람다식으로 바꿔서 코드를 완성해보자
#include <functional>
#include <iostream>
using namespace std;

template <typename T>
int arrFnMin(const int arr[], int n, T f) {
    int min = f(arr[0]);
    for (int i = 1; i < n; i++) {
        if (f(arr[i]) < min) {
            min = f(arr[i]);
        }
    }
    return min;
}

int main() {
    int arr[7] = {3, 1, -4, 1, 5, 9, -2};
    cout << arrFnMin(arr, 7, [](int n) -> int { return n * n; }) << endl;
    cout << arrFnMin(arr, 7, [](int n) -> int { return n * (n - 15) / 2; }) << endl;
}
  • 람다식도 일반 함수처럼 타입이 존재하지만, 익명함수라서 밝혀주지 않는 것이다
  • 람다식을 저장하고 싶은 경우가 생길텐데, 이 때 타입에 대한 정보를 알아야 한다
    • ??? a = [](int n) -> int { return n * n; }
    • 다행이도 캡쳐가 없는 람다식은 함수 포인터로 변환이 될 수 있다.
      • 캡처 : 여기서 대괄호[] 안에 들어가는 것
      • 람다식의 자세한 문법에 대해서는 아래에서 다룰게요.
int main() {
    int arr[7] = {3, 1, -4, 1, 5, 9, -2};
    int (*fp)(int) = [](int n) -> int { return n * n; };
    cout << arrFnMin(arr, 7, [](int n) -> int { return n * n; }) << endl;
    cout << arrFnMin(arr, 7, [](int n) -> int { return n * (n - 15) / 2; }) << endl;
}
  • 캡처가 있을 때도 담고 싶으면, auto 라는 키워드를 사용할 수 있다.
    • auto는 타입을 알아서 컴파일러가 추정할 수 있게 해준다
    • ex) auto a = arr[1]이면 auto가 int로 지정된다
    • 람다 표현식으로 런타임에 생성된 객체를 Closure(클로저)라고 한다
int main() {
    int arr[7] = {3, 1, -4, 1, 5, 9, -2};
    auto fp = [](int n) -> int { return n * n; };
    cout << arrFnMin(arr, 7, [](int n) -> int { return n * n; }) << endl;
    cout << arrFnMin(arr, 7, [](int n) -> int { return n * (n - 15) / 2; }) << endl;
}

 

다음과 같이 초기화 할 수도 있다.

auto f_11{
     [](int a, int b) -> int {return a + b;}
 };

 

참조) 두들낙서 C/C++ 강좌

 

람다식 문법

[ capture clause ] (parameters) -> return-type  
{   
   definition of method   
}

 

일반적으로 단순히 값을 반환하는 경우 return-type을 적지 않아도 컴파일러가 알아서 명시해준다

하지만 조건문 등을 통해서 return 값이 복잡하게 다뤄지는 경우 return-type을 명시해줘야 한다

 

캡쳐

람다식과 일반 함수와 다른점이라면,

일반 함수는 일반적으로 함수의 scope 외부에 존재하는 변수에 대해서 접근할 수 없다.(물론 참조형 매개변수를 사용하는 경우를 제외하고 말하는 것이다)

이와 다르게 람다식은 외부 변수에 대해 접근할 수 있다. 이를 외부 변수를 캡쳐한다고 말한다. 

그리고 대괄호 [ capture clause ] 에서 어떻게 캡쳐를 할 것인지 정하게 된다

 

캡쳐 방식은 여러 개가 있다.

1. 아에 캡쳐를 하지 않는 경우

[]

2. 참조(Reference)로 모든 외부 변수를 캡쳐하는 경우

[&]

3. 값(value)로 모든 외부 변수를 캡쳐하는 경우

[=]

4. 특정 변수 A만 Reference로 캡쳐하는 경우

[&A]

5. 특정 변수 A만 Value로 캡쳐하는 경우

[A]

6. 특정 변수 A는 Reference로, 나머지는 변수는 Value로

[=, &A]

7. 특정 변수 A는 Value로, 나머지 변수는 Reference로

[&, A]

8. 특정 변수A는 Value로, 특정 변수 B는 Reference로, 나머지는 캡쳐 X

[A, &B]

9. 람다 표현식을 사용한 개체의 포인터를 가져온다

[this]

10. 람다 표현식을 사용한 개체 자체를 복사해서 가져온다(C++17)

[*this]

 

C++17까지는 =에서 this포인터를 암시적으로 가져왔지만, C++20부터는 this포인터를 명시적으로 가져와야 한다

 

람다 캡쳐 초기화

C++14부터 람다가 포함된 영역에 변수들이 존재해야 할 필요 없이 캡처 조항에 새로운 변수를 도입하고 초기화할 수 있다. 

클로저가 생성될 때 캡쳐하므로, 람다 표현식을 여러 번 호출해도 값이 유지된다

 

 

 

 

 

 

함수 포인터

  • 모든 명령어들은 전부 0과 1로 메모리에 저장이 되기 때문에, 메모리 저장 공간의 주소를 가지고 있다

    • 함수 포인터는 함수에 대한 포인터를 가지고 있다.
bool compare(int a, int b) {
    return a == b;
}

int main() {
    bool (*fp)(int, int);
    int *a;
}
  • int *a를 a를 역참조하면 int형이 나온다라고 해석할 수 있다.

  • 이런 것처럼 fp를 역참조를 하면 bool을 리턴하고 int 2개를 매개변수로 받는 함수다 라고 해석할 수 있다.

    • fp는 int 2개를 받고 bool을 반환하는 함수를 가리키는 포인터이다.
bool compare(int a, int b) {
    return a == b;
}

int main() {
    bool (*fp)(int, int);
    fp = &compare;
        // fp = compare;도 가능하다. &를 붙이나 안 붙이나 같은 의미로 같게 동작한다. 위와 차이가 없다
    bool res = (*fp)(2, 3);
}
  • fp를 역참조를 하게되면 compare와 같게 작동하게 된다

  • fp = compare;도 가능하다. &를 붙이나 안 붙이나 같은 의미로 같게 동작한다.

  • 선언과 동시에 초기화도 가능하다

int main() {
    int arr[3];
    bool (*fp)(int, int) = compare;
    int (*ap)[3] = &arr;
    bool res = (*fp)(2, 3);
    cout << res << endl;
}

함수 포인터를 사용하는 이유?

#include <iostream>
using namespace std;

int arrMin(const int arr[], int n) {
    int min = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < min) {
            min = arr[i];
        }
    }
    return min;
}

int main() {
    int arr[7] = { 3, 1, -4, 1, 5, 9, -2};
    cout << arrMin(arr, 7) << endl;
}
  • 만약 위 코드에서 arr의 원소를 특정 연산이나 함수를 돌린 값으로 바꾼 결과로 부터 최솟값을 구하고 싶다고 해보자

    • ex) 제곱한 값 중 최소를 구하는 함수를 만들어보자
#include <iostream>
using namespace std;

int arrMin(const int arr[], int n) {
    int min = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < min) {
            min = arr[i];
        }
    }
    return min;
}

int arrSquareMin(const int arr[], int n) {
    int min = arr[0] * arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] * arr[i] < min) {
            min = arr[i] * arr[i];
        } 
    }
    return min;
}

int main() {
    int arr[7] = { 3, 1, -4, 1, 5, 9, -2};
    cout << arrSquareMin(arr, 7) << endl;
}
  • 만약 제곱이 아니라, 더 복잡한 함수를 통해서 나온 값들의 최소를 구해야 할 필요가 있을 수 있다.

    • f(3), f(1), f(-4), f(1)... 의 최솟값을 찾을 수 있는 함수가 있으면 좋겠다

    • 함수 포인터를 사용해보자

int arrFnMin(const int arr[], int n, int (*f)(int)) {
    int min = f(arr[0]);
    for (int i = 1; i < n; i++) {
        if (f(arr[i]) < min) {
            min = f(arr[i]);
        }
    }
    return min;
}
  • 함수의 매개변수에 임의의 함수를 가리키는 함수 포인터를 받아서 호출을 해줄 수 있다.
#include <iostream>
using namespace std;

int square(int x) { return x * x; }
int myFunc(int x) { return x * (x - 15) / 2; }

int arrFnMin(const int arr[], int n, int (*f)(int)) {
    int min = f(arr[0]);
    for (int i = 1; i < n; i++) {
        if (f(arr[i]) < min) {
            min = f(arr[i]);
        }
    }
    return min;
}

int main() {
    int arr[7] = { 3, 1, -4, 1, 5, 9, -2};
    cout << arrFnMin(arr, 7, square) << endl;
    cout << arrFnMin(arr, 7, myFunc) << endl;
}
  • square()와 myFunc() 함수에서 반환한 값을 기준으로 최솟값을 찾고 있다.

  • 세제곱해주는 함수를 만들어서 최솟값을 찾을 수도 있고, 무궁무진한 아무 함수를 만들어서 사용할 수 있다.

  • 어떠한 함수든지 매개변수 타입과 리턴 타입만 갖다면 어떤 함수든지 가리킬 수 있기에 유용하다

    • 타입이 다르면 에러가 발생한다(가리킬 수 없다)

    • 이런 특징 때문에 인공지능에서 종종 사용된다고 한다(주어진 데이터에 대해 모델 성능을 측정하는 경우를 생각해보자)

참조) 두들낙서 C/C++ 강좌

예외 처리(Exception Handling)

  • 예외 Case

    • ex) 파일이 제대로 열리지 않는 경우

    • ex) 데이터 포맷이 맞지 않아 데이터를 받을 수 없는 경우

Factorial을 구하는 함수

#include <iostream>
using namespace std;

int fact(int n) {
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int main() {
    int n;
    cin >> n;
    if (n < 0) {
        cout << n << ": 음수입니다." << endl;
    }
    else {
        cout << n << "!= " << fact(n) << endl;
    }
}
  • 이렇게 사용자가 실수로 음수를 입력할 수 있다.

  • n이 음수인 경우 fact함수가 실행되지 않도록 처리를 해줘야 함

#include <iostream>
using namespace std;

int fact(int n) {
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int main() {
    int n;
    cin >> n;
    try {
        if (n < 0) {
            throw n; // n은 예외 객체
        }
        cout << n << "! = " << fact(n) << endl; 
    }
    catch (int e) {
        cout << e << ": 음수입니다. " << endl;
    }
        // ...
}
  • 예외를 발생할 수 있는 부분을 try 블럭에 넣자

  • 예외가 발생하지 않는다면 cout << n << "! = " << fact(n) << endl; 을 출력하고 catch문 아래로 간다

  • 예외가 발생하면 throw 후 catch문으로 이동하고, throw한 예외 객체를 catch문의 매개변수로 전달하여 catch문 내의 코드를 실행한다

  • 예외에 해당하는 메시지를 만들어서 던져보자

int main() {
    int n;
    cin >> n;
    try {
        if (n < 0) {
            throw to_string(n) + ": 음수입니다."; // n은 예외 객체
        }
        cout << n << "! = " << fact(n) << endl; 
    }
    catch (string e) {
        cout << e << endl;
    }
    // ...
}
  • 여기서 효율을 생각한다면 예외 객체가 이동 생성이 되도록 해줄 수 있다.
int main() {
    /* 생략 */
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • if문을 사용하지 않고 try-catch를 사용해야 하는 이유?
#include <iostream>
#include <string>
using namespace std;

int fact(int n) {
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int main() {
    int n, r;
    cin >> n >> r;
    try {
        if (n < 0) {
            throw to_string(n) + ": 음수입니다."; // n은 예외 객체
        }
        int a = fact(n);

        if (r < 0) {
            throw to_string(r) + ": 음수입니다."; // n은 예외 객체
        }
        int b = fact(r);

        if (n - r < 0) {
            throw to_string(n - r) + ": 음수입니다."; // n은 예외 객체
        }
        int c = fact(n - r);

        cout << a / b / c << endl; 
    }
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • 이렇게 factorial을 호출해줄 때마다 throw를 해줘야 한다면 그냥 if문을 사용하는 거에 비해 장점이 하나도 없다

  • 객체를 던지고 받는 과정은 함수 내에서만 일어나는게 아니라, 함수 사이에서도 일어날 수 있다.

#include <iostream>
#include <string>
using namespace std;

int fact(int n) {
    if (n < 0) throw to_string(n) + ": 음수입니다.";
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int main() {
    int n, r;
    cin >> n >> r;
    try {
        int a = fact(n);
        int b = fact(r);
        int c = fact(n - r);

        cout << a / b / c << endl; // 조합(Combination)
    }
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • fact 함수에서는 예외 객체를 던져주기만 하고, 예외 처리를 의무를 메인 함수에 넘긴다

  • 이렇게 처리 해주면 코드가 되게 간결해진다

  • 조합을 구하는 과정을 comb 함수로 모듈화해보자

#include <iostream>
#include <string>
using namespace std;

int fact(int n) {
    if (n < 0) throw to_string(n) + ": 음수입니다.";
    if (n == 0) return 1;
    return n * fact(n - 1);
}

int comb(int n, int r) {
    int a = fact(n);
    int b = fact(r);
    int c = fact(n - r);
    return a / b / c;
}

int main() {
    int n, r;
    cin >> n >> r;
    try {
        cout << comb(n, r) << endl; 
    }
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • 조합을 구하는 과정에서 fact()에서 예외가 발생하면 fact() try-catch가 없으므로 comb()로 나오고, comb()에도 try-catch가 없으므로 main으로 나오게 된다. main에 try-catch가 있으므로 main에서 예외처리를 해준다

  • 조합을 여러번 사용자가 구할 수 있도록 while문에 넣어보자

    • while문 안에 try-catch를 넣는 방법과

    • try-catch 안에 while문을 넣는 방법이 있다

  • 두 방법의 차이점에 대해서 확인해보자

int main() {
    int n, r;
    try {
        while (true) {
                cin >> n >> r;
                cout << comb(n, r) << endl; 
        }
    }
    catch (const string &e) {
        cout << e << endl;
    }
    // ...
}
  • try 안에 while문이 있는 경우 while문을 돌 다 예외가 발생하면 while문 밖으로 바로 나가게 된다.

    • 예외가 한 번이라도 발생하면 루프를 빠져나온다
int main() {
    int n, r;
    while (true) {
        try {
            cin >> n >> r;
            cout << comb(n, r) << endl; 
        }
        catch (const string &e) {
            cout << e << endl;
        }
    }
    // ...
}
  • while문 안에 try-catch가 존재하는 경우 예외가 발생해도 while문이 끝나지 않고 계속 루프를 돈다

    • 예외가 발생하더라도 루프를 빠져나오지 않는다
  • catch를 여러 개를 만들 수 있다.

    • 예외를 다루는 클래스를 여러 개를 만들고 예외를 계층적으로 다룰 때, 보통 catch를 여러 개를 만들어 사용한다
int main() {
    int n, r;
    while (true) {
        try {
            throw 123;
            cin >> n >> r;
            cout << comb(n, r) << endl; 
        }
        catch (const string &e) {
            cout << e << endl;
        }
        catch (int e) {
            cout << e << endl;
        }
    }
    // ...
}
  • 프로그램을 실행하면 throw 123을 먼저 만나서 catch (int e)로 진입하게 된다

  • catch에서 잡을 수 없는 에러가 발생한다면?

    • 위 예시에서 double을 throw한다면?
int main() {
    int n, r;
    while (true) {
        try {
            throw 12.3;
            cin >> n >> r;
            cout << comb(n, r) << endl; 
        }
        catch (const string &e) {
            cout << e << endl;
        }
        catch (int e) {
            cout << e << endl;
        }
    }
    // ...
}
  • catch 안으로 들어가지 않아 런타임 에러가 발생한다

  • 다음과 같이 처리할 수도 있다.

int main() {
    int n, r;
    while (true) {
        try {
            throw 12.3;
            cin >> n >> r;
            cout << comb(n, r) << endl; 
        }
        catch (const string &e) {
            cout << e << endl;
        }
        catch (int e) {
            cout << e << endl;
        }
        catch (...) {
            cout << "알 수 없는 예외 발생" << endl;
        }
    }
    // ...
}
  • catch하지 못한다면 swtich문의 default 처럼 catch(...) 으로 진입한다

참조) 두들낙서 C/C++ 강좌

+ Recent posts