본문 바로가기

💻 내 소개 안녕하세요 엄청짱 프로그래머 손다빈 입니다.
  • 나이 : 96년생
  • 특이사항 : MZ세대, INFJ, 오른손잡이, 아이폰 유저
  • 좋아하는 음식 : 햄버거피자치킨솥뚜껑삼겹살떡볶이오튀김밥
  • 취미 : 개발, Programming, 코딩, 프로그래밍, Coding

🥷기술
Unity
Godot
Cpp
Javascript
D3
Vue

🐱 우리집 고양이 소개
츄르 먹은 후 츄르 먹기 전
  • 이름 : 콜라
  • 나이 : 8살
  • 종 : Nado moreum

📱 개인 프로젝트
🏢 참여한 프로젝트
빌런즈 Life is Pair 도씨어부키우기 직장상사혼내주기 서바이벌빙고 SlitherCoin

🌱 내 잔디밭

C++ 에서 함수를 인자로 전달하는 방법 본문

글 묶음/지옥에서 온 C++

C++ 에서 함수를 인자로 전달하는 방법

초긍정 개발자 다빈맨 2019. 4. 3. 00:00

| C++ 에서 함수를 인자로 전달하기





C++11 이전에는 다른 함수의 인자로 함수를 전달하는 방법은 함수 포인터가 유일했습니다. C언어에서 이어져온 방식이기 때문에 C개발자들에게는 크게 불편하지 않았지만 자바스크립트처럼 함수를 객체처럼 바로 전달하는 방식과 비교했을 때 상대적으로 불편했던건 사실입니다. 하지만 일반화 프로그래밍의 시대가 열리면서 STL이 크게 개편되고 C++에서도 함수를 변수에 담는것이 상당히 간편해졌죠. 여기서는 지금의 C++에서 함수를 매개변수로 넘기는 네 가지 방법에 대해서 소개합니다.


- 함수 포인터 사용하기

- 함수 객체 사용하기

- 람다 표현식 사용하기

- function<T> 템플릿 클래스 사용하기




| 함수 포인터 사용하기


함수도 당연히 메모리에 올라가고 C++에서는 이 메모리의 주소를 포인터 변수로 참조할 수 있습니다.

#include <iostream>
using namespace std;

void greet()
{
    cout << "hallo!" << endl;
}

int main()
{
    void (*fptr)() = greet; //배열과 마찬가지로 함수의 이름으로 주소를 얻을 수 있습니다.
    fptr(); // "hallo!" 출력

    return 0;
}

다음과 같은 규칙으로 함수 포인터 변수를 정의할 수있습니다. 


반환형식 (*포인터변수명)(매개변수)


그럼 매개변수와 반환형식이 포함된 예제를 하나 더 살펴보겠습니다.

#include <iostream>

using namespace std;

int sum(int lv, int rv)
{
    return lv + rv;
}

int main()
{
    int (*calc)(int, int) = sum; //int 형 인자 두개를 받고 int 형 데이터를 반환하는 함수 포인터
    cout << calc(1, 5) << endl; // "6" 출력

    return 0;
}

이제 함수 포인터를 사용하면 함수의 매개변수를 통해서도 함수를 전달할 수 있겠죠?

#include <iostream>

using namespace std;

int sum(int lv, int rv)
{
    return lv + rv;
}

void calc(int (*fptr)(int, int), int lv, int rv) 
{
    cout << fptr(lv, rv) << endl;
}

int main()
{
    calc(sum, 1, 5); //드디어 매개변수를 통해 함수를 전달할 수 있습니다.

    return 0;
}

위와 같이 함수를 매개변수로 전달하면 sum이 아닌 두개의 정수 피연산자를 다루는 계산관련 함수라면 calc 라는 함수에 전달해서 연산을 진행시킬 수 있습니다.




| 함수 객체(Functor, 펑터) 사용하기


함수 객체는 이름에서 부터 알 수 있듯이 객체(Class) 자체를 함수처럼 사용하는 방법을 의미합니다. 함수 객체를 만드는 방법은 간단합니다. 그냥 클래스에 operator()() 를 오버로딩하기만 하면 됩니다. 

#include <iostream>

using namespace std;

class Greet {
public:
    void operator()() {
        cout << "hallo!" << endl;
    }
};

int main()
{
    Greet greet;
    greet(); // "hallo!" 출력

    return 0;
}

함수 객체의 장점은 클래스이기 때문에 데이터를 객체 자체가 가지고 있을 수 있고 아래 예제처럼 여러버전의 operator()() 를 추가해서 확장할 수 있습니다.

#include <iostream>

using namespace std;

class Greet {
public:
    void operator()() {
        cout << "hallo!" << endl;
    }
    void operator()(const char* str) {
        cout << str << endl;
    }
};

int main()
{
    Greet greet;
    greet();
    greet("안녕!"); // 와아!

    return 0;
}

함수 객체를 함수로 전달하는 방법도 일반적인 객체를 전달하는 방법과 같습니다. 참조를 통해서 전달하면 간단합니다. 

#include <iostream>

using namespace std;

class Greet {
public:
    void operator()() {
        cout << "hallo!" << endl;
    }
    void operator()(const char* str) {
        cout << str << endl;
    }
};

void func(Greet& greet) {
    greet();
}

int main()
{   Greet greet;
    func(greet);
    return 0;
}




| 람다 표현식 사용하기


람다 표현식은 익명 함수를 정의할 때 사용합니다. 익명 함수이기 때문에 함수의 이름을 갖지 않습니다. 보통 재활용하지 않는 함수를 빠르게 정의해서 사용할 때 유용합니다. 람다 표현식을 통해 함수를 정의하는 규칙은 다음과 같습니다.


[캡처절] (매개변수) mutable -> 반환형식 { 함수내용 }


대괄호 [] 는 람다의 시작을 의미합니다. 자세한 설명을 먼저 하기보다는 예제를 먼저 보는게 좋겠네요.

#include <iostream>

using namespace std;

int main()
{   
    auto calc = [](int lv, int rv) -> int {
        return lv + rv;
    };
    
    cout << calc(1, 4) << endl; // "5" 출력
    
    return 0;
}

int 형식의 매개변수 두개를 받아서 더한 값을 int 형식으로 반환하는 함수를 람다 표현식으로 정의한 모습입니다. 람다 표현식 자체는 타입을 알기는 어렵기 때문에 보통 컴파일 단계에서 타입을 추론해주는 auto 키워드와 많이 사용됩니다. 위에서 보여준 람다표현식 정의 규칙을 살펴보면 mutable 이라는 키워드가 있는데 이 키워드는 옵션이기 때문에 사용하지 않아도 괜찮습니다.

auto calc = [](int lv, int rv) {
        return lv + rv;
    };

또, 예제에서 쓰인 '-> 반환형식' 도 생략 가능합니다. 함수 안에서 값을 반환하지 않으면 알아서 void 로 추론하고, 반환한다면 반환하는 타입으로 추론됩니다. 이제 대괄호에 쓰이는 캡처절과 mutable에 대해서만 이해하면 될 것 같습니다. 

#include <iostream>

using namespace std;

int main()
{   
    int num1 = 3, num2 = 5;
    
    auto calc = [num1, &num2](int addVal) {
        num2 += num1 + addVal;
    };
    
    calc(5);
    cout << num2 << endl; // num2값이 변경되었습니다!
    
    return 0;
}

캡처절이라고 하는 대괄호 [] 안에 변수목록이 들어갔네요. 람다 함수의 안에서 밖에 선언된 변수에 접근할 수 있도록 할 때 사용합니다. 기본적으로 캡처목록에 변수를 넣으면 복제해서 전달하지만 위 예제의 &num2 처럼 참조로 넣게되면 참조로 전달됩니다. 알다시피 참조로 전달할 경우 함수 안에서 값을 직접적으로 변경할 수 있게됩니다. 때문에 num2 를 함수 내부에서 변경하고 밖에서 출력해보면 값이 변경되어 있다는걸 확인할 수 있습니다. 이 예제를 다음과 같이 조금 수정해보도록 합시다.

#include <iostream>

using namespace std;

int main()
{   
    int num1 = 3, num2 = 5;
    
    auto calc = [num1, &num2](int addVal) {
        num1 += addVal; // 컴파일 에러! 이유는 캡처로 복제한 값을 가져오면 변경할 수 없습니다.
        num2 += addVal;
    };
    
    calc(5);
    cout << num1 << endl;
    cout << num2 << endl;
    
    return 0;
}

코드를 조금 수정했습니다. 기존에는 참조로 캡처절에 전달한 num2 값만 변경했는데 이번에는 num1 값도 람다함수 안에서 변경하려는 시도를 해보았습니다. 결과는 컴파일 에러가 발생하는데, 이유는 캡처로 복제한 값은 오로지 읽기만 가능하기 때문에 값을 변경할 수 없습니다. 변경하고 싶을 때 참조를 사용하면 되겠지만 한가지 방법이 더 있습니다. mutable 키워드를 사용하면 됩니다.

#include <iostream>

using namespace std;

int main()
{   
    int num1 = 3, num2 = 5;
    
    auto calc = [num1, &num2](int addVal) mutable {
        num1 += addVal; //에러가 사라졌네?
        num2 += addVal;
    };
    
    calc(5);
    cout << num1 << endl; //그래도 num1 값은 그대로잖아..
    cout << num2 << endl;
    
    return 0;
}

위 예제에서 람다 표현식에 mutable 키워드만 추가했습니다.  그리고 소스코드를 실행시키면 컴파일 에러는 사라졌습니다! 하지만 num1 값이 실제로 바뀌지는 않았습니다. 보았듯이 mutable 키워드는 복제된 캡처절에 속한 변수를 수정가능한 상태로 변경해줍니다. 다만 값 자체는 복제된 값을 변경하기 때문에 실제로 복제 원본인 함수 밖의 num1 값이 바뀌지는 않습니다. call by value 의 기본 작동방식과 동일하죠? 그리고 mutable 키워드를 사용했을 때 가지는 특성 한가지가 더 있습니다.

#include <iostream>

using namespace std;

int main()
{   
    int result = 0;
    
    auto add = [result]() mutable {
        result++;
        
        if (result == 2) {
            cout << "result가 2입니다." << endl;
        }
    };
    
    add();
    add();
    return 0;
}

mutable 키워드를 사용하면 캡처절에서 복제해서 가져온 변수의 값이 람다 표현식의 내부에서는 실행 이후에도 값이 유지되는 특성이 있습니다. 이 특성으로 인해서 예제를 보면 add() 함수를 두번 호출할 때 람다 내부의 result 값은 두번째 호출에서도 유지되기 때문에 값이 2가 되면서 "result가 2입니다." 메세지가 출력됩니다. 이렇게 람다 내부에서 값을 유지하는 형식을 가지는 변수를 클로저 변수라고 합니다.

#include <iostream>
#include <algorithm>
#include <array>
#include <iterator>

using namespace std;

int main()
{   
    auto arr = array{0, 0, 0};
    int sum = 0;
    generate(begin(arr), end(arr), [sum]() mutable { return sum++; });
    copy(begin(arr), end(arr), ostream_iterator(cout, ""));
    return 0;
}

람다 표현식은 STL 의 알고리즘과 함께 사용할때도 유용합니다. 함수를 인자로 받도록 설계되어 있는 함수가 많아서 람다를 전달하면 적은 코드로 작성이 가능하기 때문입니다. 위 예제에서는 generate 라는 인자로 전달한 함수의 결과로 컨테이너를 초기화해주는 함수 템플릿을 사용해 보았습니다.




| function<T> 템플릿 클래스 사용하기


보통 람다 표현식으로 정의한 익명 함수든, 함수 객체든 함수 포인터든 뭐든간에 일단 auto 키워드를 사용하면 변수에 함수를 담아두는건 어렵지 않습니다. 그런데 클래스 멤버변수를 정의할 때 auto를 사용할 수 없다는건 알고계실겁니다.

class FuncWrap {
public:
    auto func;
};

auto 키워드로 변수를 선언할 때 반드시 선언과 동시에 초기화가 이루어져야 하기 때문인데 이렇게 클래스의 멤버변수에 함수를 담으려면 구닥다리 방식인 함수 포인터를 사용하거나 함수객체를 사용합니다. 그런데 모든 함수형식에 대한 범용 함수 랩퍼 클래스가 이미 표준 라이브러리에 존재합니다. 바로 function<T> 템플릿 클래스죠.

#include <iostream>
#include <functional>

using namespace std;

class Siren {
public:
    void operator()() {
        cout << "beep-beep-" << endl;
    }
};

void greet() {
    cout << "hallo!" << endl;
}

int main()
{   
    function func1 = [](int lv, int rv){ return lv+rv; };
    function func2 = greet;
    function func3 = Siren();
    
    cout << func1(1, 2) << endl;    // "3" 출력
    func2();                        // "hallo!" 출력
    func3();                        // "beep-beep-" 출력
    return 0;
}

functional 헤더에 정의된 function<T> 템플릿 클래스를 이용하면 거의 모든 종류의 함수를 담을 수 있습니다. 



'글 묶음 > 지옥에서 온 C++' 카테고리의 다른 글

2차원 배열의 포인터 형  (0) 2020.06.30
C++ 형변환 연산자 완벽 정리  (4) 2019.04.21
참조가 참 조으다  (0) 2019.02.28
배열과 포인터는 다릅니다.  (2) 2019.02.14