반응형

일반 함수는 [반환형], [함수 이름], [인수] 를 입력하여 선언한다. 

하지만 람다 함수는 이름이 없는 익명 함수로써 아래의 형태로 선언하여 사용하며, std::function 과 함께 사용시 재사용도 가능하다. 

(참고 : std::function - 함수 포인터를 대체하는 키워드)

 

1. 사용법

- 람다 함수는 캡처(Capture), 인자(Parameter), 반환형(Return-Type), 바디(Body) 로 구성되어 있다.
- 캡처와 인자는 콤마(,)로 구분하여 여러 개 지정할 수 있다.

[capture] (parameter) -> return-type { body };

 

(1) 캡처(Capture)

- 캡처는 람다 함수에서 사용할 변수나 상수를 참조나 복사의 형태로 찍어오는 것이다.
- 전역 변수를 참조로 캡처할때는 [&], 복사로 캡처할때는 [=]를 사용한다.
- 지역 변수를 참조로 캡처할때는 [&변수명], 복사로 캡처할때는 [=변수명]을 사용한다.

[&]() { return; } // 모든 변수를 참조로 캡처
[=]() { return; } // 모든 변수를 복사로 캡처
[&var1, var2]() { return; } // var1은 참조, var2는 복사로 캡처
[=, &var1]() { return; } // var1만 참조로 캡처하고, 다른 변수는 복사로 캡처
[&, var2]() { return; } // var2만 복사로 캡처하고, 다른 변수는 참조로 캡처

 

- 복사로 캡처된 변수는 기본적으로 constexpr 타입으로 지정되어 값의 변경이 불가하다.

- mutable로 선언하면 값의 변경이 가능하다.

int var1{0};

[var1]() {
    var1 = 999; // 에러!!! - var1은 constexpr 타입이므로 변경이 불가
}();
[var1]() mutable {
    var1 = 999; // var1은 mutable로 선언되어 변경이 가능, 단 실제 값은 변하지 않음
    std::cout << var1 << std::endl; // 999
}();

std::cout << var1 << std::endl; // 0

 

(2) 인자(Parameter)

- 일반 함수의 인자처럼 람다 함수에서 사용할 인자를 지정한다.

[](int a) -> int { return a; } // 1개의 인자를 설정
[](int a, int b) -> int { return (a+b); } // 2개의 인자를 설정


(3) 반환형(Return-Type)

- 람다 함수의 실행 결과를 반환할 반환 타입을 지정한다.


(4) 바디(Body)

- 람다 함수에서 수행할 작업을 기술한다.

 

2. 사용 예제

#include <iostream>
#include <functional>

int main()
{
    // 간단한 람다 함수
    []() {
        std::cout << "Simple Lamda Function" << std::endl;
    }();
    
    // 인자 2개를 더해서 sum에 저장하고, 더한 값을 반환하는 람다 함수
    int sum{0};
    // std::function 에 람다 함수를 정의
    std::function<int(int,int)> fSum{[&sum](int a, int b) -> int {
        sum = a + b;
        return sum;
    }};

    std::cout << fSum(10, 20) << std::endl; // 30
    std::cout << sum << std::endl; // 30
    
    return 0;
}

 

3. 사용 예제 (클래스에서 사용)

#include <iostream>
#include <functional>
#include <string>

class Person
{
public:
    Person(std::string&& pName, int pAge, std::function<std::string(std::string&, int)>&& pFunc)
        : mName(std::move(pName))
        , mAge(pAge)
        , mTagFunc(std::move(pFunc)) // 람다 함수로 전달받은 임시 함수(r-value)를 std::move를 사용해 저장
    {
        // 출력 람다 함수에서 클래스의 멤버 변수를 사용하기 위해 this를 캡처한다.
        mPrintFunc = [this]() {
            std::cout << "Name: " << mName << ", Age: " << mAge
                << ", Tag: " << mTag << std::endl;
        };
    }

    void executeTagFunc()
    {
    	// 태그 생성 람다 함수 호출
        mTag = mTagFunc(mName, mAge);
    }

    void executePrintFunc()
    {
    	// Person 정보 출력 람다 함수 호출
        mPrintFunc();
    }

private:
    std::function<std::string(std::string&, int)> mTagFunc{};
    std::function<void()> mPrintFunc{};

    std::string mName{};
    std::string mTag{};
    int mAge{0};
};

int main()
{
    // 객체 생성시 [이름], [나이], [태그 생성 함수]를 인자로 받음
    Person ps("Simson", 44, [](std::string& pName, int pAge) -> std::string {
        return std::string(pName + "-" + std::to_string(pAge));
    });

    ps.executeTagFunc();
    ps.executePrintFunc();

    return 0;
}


 

반응형
반응형

std::function은 기존의 함수 포인터를 대체하는 키워드로 C++11에서 추가되었으며, 잘 사용하면 짧은 코드로 많은 기능을 구현할 수 있다.

 

1. 사용법

#include <functional>

std::function< return-type (parameter-type) > var-name = function;

 

(1) 인자

  1-1) return-type : 함수의 실행 결과를 반환할 반환 타입을 지정

  1-2) parameter-type

    - 함수에서 사용할 인자의 타입을 지정

    - 인자가 여러 개일 경우 콤마(,)로 구분

 

2. 사용 예제 (1)

#include <iostream>
#include <functional>

void simpleFunc()
{
    std::cout << "Simple Function" << std::endl;
}

void executeFunc(std::function<void()>& func)
{
    func();
}

int main()
{
    // 기존 함수로 생성
    std::function<void()> func{simpleFunc};

    // 람다 함수로 생성
    std::function<void()> lambdaFunc{[](){
        std::cout << "Simple Lambda Function" << std::endl;
    }};

    // 함수 실행
    func();
    lambdaFunc();

    // 함수의 인자로 넘겨서 사용
    executeFunc(func);
    executeFunc(lambdaFunc);
    
    return 0;
}

 

3. 사용 예제 (2)

#include <iostream>
#include <functional>

std::function<void(unsigned char)> moveLEFT{[](unsigned char key){
    std::cout << "[" << key << "] move LEFT" << std::endl;
}};

std::function<void(unsigned char)> moveRIGHT{[](unsigned char key){
    std::cout << "[" << key << "] move RIGHT" << std::endl;
}};

std::function<void(unsigned char)> moveUP{[](unsigned char key){
    std::cout << "[" << key << "] move UP" << std::endl;
}};

std::function<void(unsigned char)> moveDOWN{[](unsigned char key){
    std::cout << "[" << key << "] move DOWN" << std::endl;
}};

void keyHandler(const unsigned char key)
{
    switch (key) {
        case 'a': return moveLEFT(key);
        case 'd': return moveRIGHT(key);
        case 'w': return moveUP(key);
        case 's': return moveDOWN(key);
    };
}

int main()
{
    unsigned char key{};

    while (true)
    {
        std::cout << "Input key: ";
        std::cin >> key;

        if (key == 'q') break;

        keyHandler(key);
    }


    return 0;
 }


 

 

반응형
반응형

기존 C++ 포인터는 동적 할당 메모리를 생성(new) 해서 사용한 후 반드시 삭제(delete) 해줘야 한다.

프로그래머가 수동으로 관리하다보니 삭제가 누락되면 메모리 릭이 발생하곤 한다.

C++11 에서 추가된 Smart Pointer를 사용하면 메모리 삭제에 대한 부분을 신경쓰지 않아도 된다.

Smart Pointer에는 3가지(unique_ptr, shared_ptr, weak_ptr)가 있으며, weak_ptr은 사용 빈도가 매우 낮으므로 unique_ptr shared_ptr 에 대해 알아보자.

 

1. 사용법

#include <memory>

std::unique_ptr< var-type > var;
std::shared_ptr< var-type > var;

 

(1) 인자

  1-1) var-type : 변수나 객체의 타입을 지정

 

2. 일반 포인터 사용

- 포인터를 생성하여 사용한 후 반드시 삭제를 해줘야 한다.

- 프로그래머의 실수로 삭제가 누락될 경우 메모리 릭이 발생할 우려가 있다.

#include <iostream>
#include <string>

using Type = std::string;

int main()
{
    Type* str{new Type("Hello world !!!")};
    std::cout << *str << std::endl;
    
    delete str; // 동적 메모리 사용 후 삭제 필요

    return 0;
}

 

3. std::unique_ptr

- 소유권을 하나만 가질 수 있는 포인터이다.

- 객체를 선언하여 사용한 후 사용 범위가 종료될 때 소멸자가 호출되어 메모리를 자동으로 삭제한다. 

- 소유권을 이전하기 위해서는 std::move()를 사용해야거나 참조로 전달해야 한다.

- C++14 이후에는 std::make_unique() 함수를 통해 포인터를 생성할 수 있다.

#include <iostream>
#include <string>
#include <memory>

using Type = std::string;

void print(std::unique_ptr<Type>& pStr4)
{
    std::unique_ptr<Type> pStr5{std::move(pStr4)};
    std::cout << pStr4 << std::endl; // 0
    std::cout << pStr5 << std::endl; // 0x141fc20
    
    // 함수가 종료되면 pStr5는 삭제됨.
}

int main()
{
    std::unique_ptr<Type> pStr{new Type("Hello world !!!")};
    //std::unique_ptr<Type> pStr{std::make_unique<Type>("Hello world !!!")}; // C++14 이후
    std::cout << pStr << std::endl; // 0x141fc20
    
    Type* ppStr{pStr.get()}; // 주소값 가져오기
    std::cout << ppStr << std::endl; // 0x141fc20
    
    //std::unique_ptr<Type> pStr2{pStr}; // 컴파일 에러!! (소유권은 하나만 가능)
    std::unique_ptr<Type> pStr3{std::move(pStr)}; // 소유권 이전 가능
    std::cout << pStr << std::endl; // 0
    std::cout << pStr3 << std::endl; // 0x141fc20
    
    print(pStr3); // 인자로 넘겨서 참조로 받기 가능
    std::cout << pStr3 << std::endl; // 0
    
    // 동적 메모리 사용 후 삭제가 필요하지 않음.

    return 0;
}

 

4. std::shared_ptr

- 소유권을 여러개 가질 수 있는 포인터이다.

- 소유자 수의 관리는 내부적으로 참조 카운터(Reference Counter)를 통해 이루어진다.

- 참조 카운터가 0이 되면 메모리를 자동으로 삭제한다.

- C++14 이후에는 std::make_shared() 함수를 통해 포인터를 생성할 수 있다.

#include <iostream>
#include <string>
#include <memory>

using Type = std::string;

void print(std::shared_ptr<Type>& pStr3)
{
    std::shared_ptr<Type> pStr4{std::move(pStr3)};
    std::cout << pStr4 << " - " << pStr4.use_count() << std::endl; // 0x2274c20 - 2
    pStr4.reset(); // 명시적으로 메모리 해제
    
    // 함수가 종료되면 참조 카운트가 1 감소됨.
}

int main()
{
    std::shared_ptr<Type> pStr{new Type("Hello world !!!")};
    //std::shared_ptr<Type> pStr{std::make_shared<Type>("Hello world !!!")}; // C++14 이후
    std::cout << pStr << " - " << pStr.use_count() << std::endl; // 0x2274c20 - 1

    Type* ppStr{pStr.get()}; // 주소값 가져오기
    std::cout << pStr << " - " << pStr.use_count() << std::endl; // 0x2274c20 - 1

    std::shared_ptr<Type> pStr2{pStr}; // 소유권 여러개 가능
    std::cout << pStr2 << " - " << pStr2.use_count() << std::endl; // 0x2274c20 - 2

    print(pStr2); // 인자로 넘겨서 참조로 받기 가능
    std::cout << pStr2 << " - " << pStr2.use_count() << std::endl; // 0 - 0

    print(pStr);
    std::cout << pStr << " - " << pStr.use_count() << std::endl; // 0 - 0
    
    // 동적 메모리 사용 후 삭제가 필요하지 않음.
    
    return 0;
}

 



 

반응형
반응형

1. 설명

- 원자적으로 처리할 변수에 대해 정의한다.

- 쓰레드 동작시 공유 변수의 동시성(Concurrency) 문제에 대해 해결해 준다.

 

 

2. 문제 상황

- 아래의 코드를 실행해보면 전역 변수 num을 2개의 쓰레드가 각각 더하거나 뺀다.

- 더하거나 빼는 과정에 문맥교환(Context Switching)이 일어나면 연산이 완료되기 전에 제어권을 빼앗길 수 있다.

- 더하거나 빼는 연산을 원자적으로 처리하기 위해 atomic 변수로 선언이 필요하다.

#include <iostream>
#include <thread>

int num{0};

void ThreadHandler(const bool isAdd)
{
    for (int i=0; i<1000000; i++)
    {
    	if (isAdd) num++;
        else num--;
    }
    
    std::cout << "num=" << num << std::endl;
}

int main()
{
    std::jthread thr1{ThreadHandler, true};
    std::jthread thr2{ThreadHandler, false};
    
    return 0;
}

 

3. 사용법

#include <atomic>

std::atomic< var-type > var;

 

(1) 인자

  1-1) var-type : 변수나 객체의 타입을 지정

 

4. 사용 예제

- 위에서 문제가 됐던 num 변수의 타입을 std::atomic<int>로 변경해 준다.

- 이제 num 변수의 원자성이 보장되기 때문에 마지막 출력값은 항상 0이 된다.

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> num{0}; //atomic 변수 선언

void ThreadHandler(const bool isAdd)
{
    for (int i=0; i<1000000; i++)
    {
    	if (isAdd) num.fetch_add(1); //atomic 값을 증가 (num++ 사용가능)
        else num.fetch_sub(1); //atomic 값을 감소 (num-- 사용가능)
    }
    
    std::cout << "num=" << num.load() << std::endl; //atomic 값을 출력
}

int main()
{
    num.store(0); //atomic 변수 초기화
    
    std::jthread thr1{ThreadHandler, true};
    std::jthread thr2{ThreadHandler, false};
    
    return 0;
}

 

 

 

 

반응형

+ Recent posts