본문 바로가기

3.구현/C or C++

[c++] C++11 보편참조 사용하기

들어가기

보편참조는 C++에서 왼값 참조와 오른값 참조를 받을 수 있는 참조이다. 그러나 특수한 조건에서만 활성화되므로 잘 작동하는 것처럼 보여도 의도한대로 동작하지 않을 수 있다. 보편 참조는 템플릿 또는 auto을 사용하는 방법이 있다. 지금부터 보편 참조에 대해서 알아보자.

작성자: ospace114@empal.com, http://ospace.tistory.com/

템플릿 활용

보편 참조를 사용하기 위해서는 템플릿 구조를 가져야 한다. 일반적인 형태는 아래와 같다.

template<typename T>
void f(T&& val) {
    cout << "f(" << typeid(val).name() << endl;
}

템플릿 함수 f() 인자의 선언 형태가 {형식}&&가 되어야 한다. 근데, 이런 형태는 어디서 많이 보았던 형태이다. 그렇다! 오른쪽 참조 선언과 동일한 형태인데, 특정한 조건이 되면 보편참조가 된다. 그래서 형식 연역이 되면서 왼값과 오른값을 모두 받을 수 있다.

기존에는 왼값 참조와 오른값 참조에 대한 함수를 별도로 정의해야 했다.

#include <iostream>
class Foo {};
void f(Foo& val) {
    std::cout << "f_l(" << typeid(val).name() << ")\n";
}
void f(Foo&& val) {
    std::cout << "f_r(" << typeid(val).name() << ")\n";
}
int main() {
    Foo foo;
    f(foo);
    f(Foo());
}

별도로 있었던 위의 함수들을 보편 참조를 사용한 함수 한 개로 정의할 수 있다.

#include <iostream>

class Foo {};
template<typename T>
void f(T&& val) {
    std::cout << "f(" << typeid(val).name() << ")\n";
}
int main() {
    Foo foo;
    f(foo);
    f(Foo());
}

함수를 중복해서 정의해야하는 수고로움이 없어졌다.

템플릿을 사용할 때에 주의할 부분이 두가지가 있다. 먼저 한정사를 붙이면 안된다. const을 붙이면 오른값 참조로 바뀌기 때문에 주의가 필요하다.

#include <iostream>

class Foo {};

template<typename T>
void f(const T&& val) {
    std::cout << "f(" << typeid(val).name() << ")\n";
}
int main() {
    Foo foo;
    f(foo); // 에러
    f(Foo());
}

const을 붙이면 오른값 참조로 변경되기 때문에 왼값에 대해서는 호출이 안되서 에러가 발생한다.

두 번째로 다른 템플릿 클래스를 사용해서 인자를 정의하는 경우도 오른값 참조로 바뀐다.

#include <iostream>

class Foo {};

template<typename T>
class Box {};

template<typename T>
void f(Box<T>&& val) {
    std::cout << "f(" << typeid(val).name() << ")\n";
}

int main() {
    Box<Foo> foo;
    f(foo); // 에러
    f(Box<Foo>());
}

이는 Box가 사용될 때에 Box 내부 T의 형식연역이 완성될 뿐이고 f() 입장에서는 이미 Box 형식이 정해져있다. 그렇기 때문에 형식연역이 발생하지 않는다.

템플릿으로 보편 참조를 사용할 경우 조건을 정리하면 다음과 같다.

  • 템플릿 함수를 사용
  • 순수한 {형식}&&으로 선언

보편참조 주의사항

fwd이라는 보편참조 템플릿 함수가 있고, 이 함수에서 f()로 완벽전달을 한다고 하자.

template<typename T>
void fwd(T&& param) {
    f(std::forward<T>(param));
}

이 fwd()로 인자를 넘기면 호출형식에 대한 정보를 그대로 f()로 전달하게 된다.

중괄호 초기화

아래와 같이 f()를 정의하고 이를 사용한 예제인 경우 어떻게 될까?

void f(const std::vector<int>& v) {}

f({1,2,3});   // (1)
fwd({1,2,3}); // (2)

(1)에서 중괄호 초기화에 의해 std::vector로 넘겨진다. 이는 자연스럽게 초기화가 되면서 특별한 문제는 없다.

(2)에서는 에러가 발생한다. 중괄호 초기화는 비연역 문맥(non-deduced context)로 인해 연역할 수 없는 형태이기 때문에 에러가 발생한다. 이를 해결하려면 auto가 중괄호 형식 연역이 가능하기 때문에 중괄호를 auto로 할당해 std::initializer_list으로 연역해서 fwd()를 호출하면 가능하다.

auto v = {1,2,3};
fwd(v);

static const 및 constexpr 멤버

클래스 멤버에 static const 또는 static constexpr인 경우를 보자.

class Box {
public:
    static constexpr std::size_t Size = 10;
};

void f(std::size_t v) {}

f(Box::Size);   // (1)
fwd(Box::Size); // (2)

일반적으로 생각할 때에는 constexpr로 되어 있는 상수는 특별하게 문제가 없을거라 생각할수 있지만 주의할 점이 있다.

(1)인 경우 f()에 값으로 전달되기 때문에 실행하는데 문제가 없다.

(2)인 경우 참조로 값을 가져오기 때문에 해당 정의가 없을 경우 링크에러가 발생한다. 참조는 주소형태로 처리되기에 미리 메모리에 로딩해야 한다. 이를 위해 자료 멤버를 사전에 정의하면 된다.

constexpr std::size_t Box::Size;

fwd(Box::Size);

이미 Box 클래스내에서 초기값을 지정했기에 별도로 초기화하지 않아도 된다.

함수 포인터

다음은 함수 포인터를 인자로 넘기는 경우를 보자.

void f(void pFn(int)) {}

void process(int val) {}
void process(int val, int pri) {}

f(process);   // (1)
fwd(process); // (2)

이번에는 process()가 오버로딩으로 정의되어 있고 이를 함수 포인터로 완벽전달하는 경우이다.

(1)은 f()에서 받을 process()의 인자 형식을 알고 있기 때문에 컴파일러가 정확한 process 함수 포인터를 전달한다.

(2)인 경우 fwd()는 process() 인자 형식 정보을 얻을 수 없기 때문에 process()을 선택할 수 없다. 그래서 에러가 발생한다. 이를 해결하려면 함수에 대한 형식 정보를 명시적으로 지정하면 된다.

using FnProcess = void (*)(int);
FnProcess fnProcess = process;

fwd(fnProcess);

만약 템플릿 함수를 함수 포인터로 넘기는 경우도 고려해보자.

template<typename T>
T processT(T param) {}

fwd(processT); // 에러
fwd(static_cast<FnProcess>(processT)); // 성공

템플릿 함수도 여러 함수를 나타나기에 어떤 형식 정보인지 알 수 없다. 이때에 static_cast를 사용해서 특정 형식 정보로 지정하면 된다.

비트 필드

이번에는 비트 필드에 대해서 살펴보자.

struct MyHeader {
    std::uint32_t version: 8, length: 16, chk: 8;
};

void f(std::size_t v) {}

MyHeader h;
f(h.length);   // (1)
fwd(h.length); // (2)

(1)은 단순 값 전달이기 때문에 에러가 없다.

(2)에서 비트 필드는 임의 요소들로 구성되어 있는데 이를 참조할 수 있는 방법이 없다. C++표준에는 “non-const 참조는 절대로 비트필드에 묶이지 않아야 한다.”라고 되어 있다. 이를 해결할 방법은 먼저 값을 복사해서 전달하면 된다.

auto length = static<std::uint16_t>(h.length);
fwd(lenght);

auto 활용

보편 참조를 사용할 수 있는 다른 방법으로 auto가 있다. auto도 템플릿 처럼 형식연역에 관여되므로 이로 인해 보편 참조를 사용할 수 있다. auto에 대한 보편참조 형식은 auto&&으로 사용하면 된다.

값을 할당할 때에도 보편 참조를 사용할 수 있다.

class Foo {};

int main() {
    Foo foo;
    auto&& foo1 = foo;
    auto&& foo2 = Foo();
}

함수의 결과가 어떤 형식인지 모를 경우 보편 참조로 받을 수 있다. 또한 함수에서도 활용할 수 있다. 주의할 부분은 일반적인 함수에서 auto 형식 인자를 선언할 수 없다. 그래서 람다식을 사용하면 가능하다.

#include <iostream>

class Foo {};

auto f = [](auto&& val) {
    std::cout << "f(" << typeid(val).name() << ")\n";
};

int main() {
    Foo foo;
    f(foo);
    f(Foo());
}

결론

보편 참조를 사용하는 방법을 살펴보았다. 보편 참조를 사용할 때에 주의할 점은 잘못 선언될 경우 오른값 참조로 바뀌기 때문에 사용할 때에 신경써야 한다. (왜? 이렇게 만들었지? ㅡ.ㅡ;;;)

그리고 추가로 만약 보편참조가 있는데 오른값 참조로 된 함수가 동시에 있어도 에러는 발생하지 않으며 호출시 컴파일러는 오른값 참조로 된 함수를 먼저 호출한다.

보편 참조를 사용할 경우 신경써야할 부분이 좀 있다. 부족한 글이지만 여러분에게 도움이 되었으면 합니다. 모두 즐프하세요. ospace.

참고

[1] 스콧 마이어스, Effective Modern C++, 인사이트

반응형

'3.구현 > C or C++' 카테고리의 다른 글

[c++] decltype, auto, 중괄호 초기화 사용하기  (0) 2023.11.25
[c++] C++14 형식연역  (1) 2023.11.21
[c++] 오른값(r-value) 참조 이해하기  (0) 2023.11.17
protothreads 정리  (0) 2023.10.25
[c++] 함수자 구현 고찰  (0) 2023.10.18