Home [C++] emc++ 04: Move, Perfect forwarding
Post
Cancel

[C++] emc++ 04: Move, Perfect forwarding

  • std::move가 모든 것을 이동하지는 않음
  • 완벽전달은 완벽하지 않음
  • 이동연산이 복사 연산보다 항상 싼 것은 아님
  • 기대한 만큼 싸지 않음
  • 이동이 유효한 문맥에서 항상 이동 연산이 호출되는 것은 아님
  • 형식&& 형태의 구성체가 항상 오른값 참조를 나타내는 건 아님

  • 매개변수는 항상 왼값이다.
    • 매개변수 타입이 오른쪽 참조인 경우에도 매개변수 자체는 왼값이다.

항목23: std::move와 std::forward를 숙지하라


  • std::move는 모든 것을 이동 x
  • std::forward는 모든 것을 전달 x
  • 실행시점에서는 둘 다 아무것도 하지 않음
  • 실행 가능 코드를 단 한 바이트도 생성하지 않음
  • 그저 캐스팅을 수행하는 함수 템플릿임

  • std::move
    • 주어진 인수를 오른값으로 캐스팅
  • std::forward
    • 특정 조건이 만족될 때에만 캐스팅을 수행

std::move

  • std::remove_reference::type&& 을 리턴
    • 항상 참조가 아닌 타입에 적용하는 &&
    • 결과적으로 std::move는 반드시 오른 값 참조를 돌려줌
    • std::move는 자신의 인수를 오른값으로 캐스팅
1
2
3
4
5
6
7
template <typename T>
typename std::remove_reference<T>::type&&
move(T&& param)
{
    using ReturnType = typename std::remove_reference<T>::type&&;
    return static_cast<ReturnType>(param);
}
  • c++14버전은 다음과 같이 구현가능
1
2
3
4
5
6
template <typename T>
decltype(auto) move(T&& param)
{
    using ReturnType = remove_reference_t<T>::type&&;
    return static_cast<ReturnType>(param);
}
  • 오른값은 그저 이동의 후보
    • std::move는 이동할 수있는 객체를 좀 더 쉽게 지정하기 위한 함수

오른값이 이동의 후보가 아닌 경우

  • const 객체
    • const 정확성을 유지하기 위해
    • ex) const std::string 타입의 오른값
      • 이동생성자에 전달 불가가
      • 이동생성자가 const가 아닌 std::string에 대한 오른값 참조를 받기 때문
      • const에 대한 왼쪽 참조를 const 오른값에 묶는것이 허용되기에 복사생성자가 호출됨
  1. 이동을 지원할 객체는 const로 선언하지 말아야함
    • const 객체에 대한 이동 요청은 복사연산으로 변환됨
  2. std::move는 아무것도 이동하지 않음, 캐스팅되는 객체가 이동 자격을 갖추게된다는 보장도 제공 x, 확실한건 결과가 오른값이라는 것

std::forward

  • std::forward

    • 특정 조건이 만족될 때에만 캐스팅
    • 조건부 캐스팅
  • 모든 파라미터는 하나의 왼값

    • 따라서 어떠한 템플릿 함수에서 보편참조 매개변수를 받아 그 것을 오버로딩함수에 전달할 때 항상 왼값으로 호출됨
      • std::forward는 이를 방지하여, 인수가 오른값이면 오른값으로 캐스팅함
  • forward가 오른값으로 초기화되었는지를 확인하는 방법

    • 그 정보가 템플릿 매개변수 T에 부호화(인코딩)되어 있음
    • 항목28 참고

차이

  • std::forward 또한 move처럼 쓸 수 있음

    • but 타자량 많음
    • 실수로 std::string& 지정할 가능성이 있음
  • std::move의 매력

    • 사용하기 편함
    • 오류의 여지가 줌
    • 코드의 명확성 증가
1
2
3
4
Widget(Widget&& rhs):
    s(std::move(rhs.s)),
    s2(std::forward<std::string>(rhs.s2)) {
}
  • std::forward를 사용한다는 것
    • 오른값에 묶인 참조만 오른값으로 캐스팅하겠다는 뜻
    • 객체를 원래의 성질을 유지한 채로 다른 함수에 넘겨주는 전달 역할

항목24: 보편 참조와 오른값 참조를 구별하라


  • T&& 는 무조건 오른값 참조가 아님
1
2
3
4
5
6
7
8
9
10
void f(Widget&& param); // param은 오른값 참조
Widget&& var1 = Widget(); // var1은 오른값 참조
auto&& var2 = var1; // var2는 오른값 참조가 아님

template<typename T>
void f(std::vector<T>&& param); // param은 오른값 참조


template<typename T>
void f(T&& param); // param은 오른값 참조가 아님

T&&의 두 가지 의미

  • 오른값 참조

    • 예상한 그대로 행동
    • 이동의 원본이 될 수 있는 객체를 지정
  • 오른값 참조 또는 왼값 참조 중 하나

    • 때에 따라서 왼값 참조처럼 행동
    • const, volatile, 비 const, 비 volatile 객체에 묶일 수도 있음

보편 참조(universal reference or forwarding reference)

  • 보편참조에는 거의 항상 std::forward 적용해야함
1
2
3
4
auto&& var2 = var1; // var2는 오른값 참조가 아님

template<typename T>
void f(T&& param); // param은 오른값 참조가 아님
  • 두 가지 문맥에서 나타나는 보편참조

    • 공통점: 타입 연역이 발생
    • 참조 선언의 형태(form)이 정확해야함 (std::vector&& 는 오른값 참조)
  • 보편참조는 참조이므로, 반드시 초기화해야함

    • 초기치에 따라 참조 or 오른값 참조

보편참조가 아닌 경우

  • const가 붙으면 오른값 참조가됨
  • 템플릿 안에서 형식이 T&& 라도, 타입 연역이 일어나지 않을 때
    • ex) vector::push_back(T&& x)
      • 인스턴스화로 push_back의 선언을 완전하게 결정하기 때문

emplace_back

  • 타입 연역을 사용
1
2
template<class... Args>
void emplace_back(Args&&... args);
  • vector 의 타입 매개변수 T와 독립적
    • 매개변수 묶음은 호출 때마다 연역됨

auto&&

  • 보통 람다 표현식에서 auto&& 매개변수 선언
    • 거의 모든 함수의 실행시간을 측정하는 함수 구현 가능
      • 왼값, 오른값 둘 다 가능하기 때문

기억해 둘 사항들

  • 보편참조

    • 함수 템플릿 매개변수의 타입이 T&& 형태, T가 연역된다면 보편참조
    • auto &&로 선언한다면, 보편참조
  • 오른값 참조

    • 타입 선언이 정확히 T&& 가 아니면
    • 타입 연역이 일어나지 않으면
  • 보편참조는 초기화 값에 따라 오른값, 왼값 결정

항목25: 오른값 참조에는 std::move, 보편 참조에는 std::forward 사용


  • 오른값 참조에 move

    • 이동할 수 있음이 확실하기 때문
  • 보편참조에 forward

    • 보편참조는 오른값 or 왼값.. 중의성을 가지기 때문
  • 오른값 참조를 다른 함수로 전달할 때에는 오른값으로 무조건 캐스팅 해야함

    • 그런 경우 항상 오른값에 묶이기 때문
  • 보편 참조를 다른 함수에 전달할 때 오른값으로의 조건부 캐스팅을 적용해야함

    • 특정 조건하에서만 오른값으로 묶이기 때문

보편참조에 std::move 사용 X

  • 왼값이 의도치 않게 수정되는 결과가 나올 수 있음
  • 보편참조를 받는 하나의 템플릿을 왼값 참조와 오른값 참조들에 대해 오버로드한 두 개의 함수로 대체하면, 실행 시점의 추가비용을 유발할 가능성이 큼
    • 문제점: 소스코드 크기 + 관용구 위반 + 코드의 실행시점 성능
    • 심각한 문제점: 설계의 규모 변성(scalability)이 나쁨
      • 매개변수가 n개이면…. 오버로드 버전의 개수를 증가시켜야함

std::move, std::forward 는 마지막에

  • move, 오른값 참조로 해석한 forward는 값을 이동시키기 때문

return by value

  • 결과를 값으로 돌려준다면, 그 것이 오른값 참조나 보편 참조에 묶인 객체라면, 해당 참조를 돌려주는 return 문에서 std::move나 std::forward를 사용하는 것이 바람직

RVO

  • 반환값 최적화

    • 지역변수 w를 함수의 반환값을 위해 마련한 메모리 안에 생성한다면 w의 복사를 피할 수 있음
    • 단 다음 조건에 만족해야함
      1. 지역 객체의 타입이 반환타입과 같아야함
      2. 그 지역객체가 바로 함수의 리턴값이어야함
  • return std::move(w) 는 조건2에 맞지 않음

    • 컴파일러가 할 수 있는 최적화 여지를 제한함
    • 반환값 최적화의 필수조건들이 성립했지만, 컴파일러가 복사 제거를 수행하지 않기로 한 경우, 반환되는 객체는 반드시 오른값으로 취급되어야 한다는 게 표준

기억해 둘 사항들

  • move, forward는 마지막에 쓰이는 지점에서 사용
  • 결과를 값 전달 방식으로 돌려주는 함수가 오른값 참조나 보편 참조를 돌려줄 때에도 move, forward 사용
  • RVO의 대상이 될 수 있는 지역 객체에는 절대로 move, forward 사용 x

항목26: 보편 참조에 대한 오버로드를 피하라


1
2
3
4
5
6
7
8
9
10
11
// 보편참조가 아닌경우 생기는 비용
std::multiset<std::string> names;
void logAndAdd(const String& name) {
  names.emplace(name);
}

std::string s("hello");

logAndAdd(s); // 왼값: emplace는 복사
logAndAdd(std::string("hello")); // 오른값 std::string, emplace는 복사, 복사를 피하고 이동을 수행할 방법이 있음
logAndAdd("hell") // 문자열 리터럴, 암묵적으로 생성된 임시 std::string, name에 복사되나 emplace에 문자열 리터럴을 직접 전달했으면, emplace는 직접 string 객체를 multiset안에서 생성 가능
  • 보통의 오버로딩 해소 규칙

    • 정확한 부합이 승격(promotion)을 통한 부합보다 우선시된다.
    • 따라서 암묵적 타입변환이 있는경우 보편 참조 오버로딩이 호출됨
  • 보편참조 오버로딩은 훨씬 많은 인수 타입들을 받아들임

    • 보편참조에 대한 오버로드를 만드는것은 나쁜선택
  • 완벽 전달 생성자

    • 복사나 이동에 해당하는 템플릿 생성자가 인스턴스화되어도, 복사 생성자나 이동생성자들은 자동생성이 일어남
    • 어떤 함수 호출이 템플릿 인스턴스와 비템플릿 함수가 똑같이 부합한다면, 비템플릿 함수를 우선시한다는 규칙
      • 이로인해 자동생성된 생성자 호출됨

항목27: 보편 참조에 대한 오버로드 대신 사용할 수 있는 기법들


오버로딩 포기

  • 각자 다른 이름 붙이기
    • but 생성자에서는 불가

const T& 매개변수를 사용

  • 보편 참조 매개변수 대신 const에 대한 왼값 참조 매개변수 사용
    • 단점: 효율적이지 않음
    • but 예상치 않은 문제 회피

값 전달 방식의 매개변수를 사용

1
explict Person(std::string n) : name(std::move(n)) {} // 이동 생성자를 대체
  • 복잡도를 높이지 않고, 성능을 높이는 방법
    • 항목 41: 복사될 것이 확실한 객체는 값으로 전달

꼬리표 배분(tag dispatch)을 사용

  • const 왼값 참조 전달이나 값 전달은 완벽 전달을 지원하지 않음
    • 꼭 이를 지원해야할 경우
1
2
3
4
5
6
7
std::multiset<std::string> names;
template<typename T>
void logAndAdd(T&& name) {
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplace(std::forward<T>(name));
}
  • 위 함수에서, 오버로딩을 하는 대신 logAndAdd가 호출을 다른 두 함수로 위임하게 한다. (정수값을 위한, 그 외 모든 것을 위한)
1
2
3
4
5
6
7
8
9
10
template<typename T>
void logAndAdd(T&& name) {
  logAnddAddImpl(std::forward<T>(name), typename std::is_integral<std::remove_reference<T>::type>());
}

template<typename T>
void logAndAdd(T&& name, std::false_type); // 비 정수 인수

template<typename T>
void logAndAdd(T&& name, std::true_type); // 정수 인수
  • is_integral은 오른값인 정수 인수들에 대해서는 작동하지만, 왼값 int가 전달되면 T는 int&로 연역됨
    • 그러므로 std::remove_reference::type 사용

보편 참조를 받는 템플릿을 제한

  • 꼬리표 배분의 필수요소: 클라이언트 API역할을 하는 단일한 함수

    • 이 함수는 요청된 작업을 구현함수로 배분
    • 이는 생성자에서 불가
  • std::enable_if 사용

    • 특정 템플릿이 존재하지 않는것 처럼 행동하게 만들 수 있음
      • 비활성 템플릿이라 부름
    • 오직 지정된 조건이 만족될 때에만 활성화됨
    • SFINAE 덕분에 작동함
1
2
3
4
5
class Person {
  template<typename T,
  typename = typename std::enable_if<조건>::type>
  explicit Person(T&& n);
}
  • !std::is_same<Person, typename std::decay<T>::type>::value

    • is_same == 두 타입이 같은지 판단
    • 이를 조건으로 삼으면 Person&& 생성자 비활성(템플릿 이동, 복사 생성자 비활성)
    • But, 여기서 참조여부, volatile, const 를 제거해야함
      • std::decay 사용
  • 문제점

    • 이와 같은 생성자를 가진 클래스를 상속받는 클래스에서의 이동과 복사 생성에서 문제 생김
    • 해결: !std::is_base_of<Person, typename std::decay<T>::type>::value
  • 정수와 비정수 구별

    • 정수인수들을 처리하는 생성자를 오버로딩
    • 그런 인수들에 대해서는 템플릿화된 생성자가 비활성화되로록 하는 조건을 추가
1
2
3
4
5
6
7
8
9
10
11
12
template<typename T,
typename = typename std::enable_if<
  !std::is_base_of<Person, std::decay_t<T>>::value
  &&
  !std::is_integral<std::remove_reference_t<T>>::value
  >
>
explict Person(T&& n): name(std::forward<T>(n));

explicit Person(int idx)
: name(nameFromIdx(idx));

절충점들

  • 완벽전달이 더 효율적이라는 점은 하나의 규칙

    • 임시 객체를 생성하는 비효율성이 없기 때문
  • 완벽전달 단점

    • 완벽전달 불가능한 인수들이 있음 (항목30)
    • 유효하지 않은 인수를 전달했을 때 나오는 오류 메시지가 난해
      • 보편 참조를 성능 최우선적인 인터페이스에만 사용하는 이유
  • static_assert를 사용하여 T 객체로부터 ~를 생성할 수 있는지 점검 가능

    • std::is_constructible

기억해 둘 사항들

  • 보편 참조와 오버로딩의 조합에 대한 대안

    • 구별되는 함수 이름 사용
    • 매개 변수를 const에 대한 왼값 참조로 전달
    • 꼬리표 배분 사용 등
  • std::enable_if

    • 템플릿 인스턴스화 제한
  • 보편참조 매개변수는 효율성 면에서 좋지만, 사용성 면에서 단점

항목28: 참조 축약(reference collapsing)을 숙지하라


  • 보편 참조는 아래와 같이 추론됨
1
2
void func(Widget& && param); //1
void func(Widget && param); //2
  • C++은 레퍼런스에 대한 레퍼런스를 허용하지 않음

  • But, 특수한 경우에는 참조 축약이라는 특정한 규칙에 의해 레퍼런스로 만듦

    1. 둘 중 하나라도 lvalue ref 면 결과는 lvalue ref
    2. 그 외의 경우 rvalue ref

std::forward 원리

  • 참조축약 규칙을 이용하여 lvalue, rvalue 구분

왼값 참조가 올 경우

1
2
3
4
Widget& && forward(remove_reference_t<Widget&>& param)
{
    return static_cast<Widget& &&>(param);
}
  • 파라미터 타입은 remove_reference 로 인해 참조 제거
  • 리턴 타입은 참조 축약 규칙에 의해 참조로

오른값 참조가 올 경우

1
2
3
4
Widget&& forward(remove_reference_t<Widget>& param)
{
    return static_cast<Widget&&>(param);
}
  • 타입 T 는 Widget으로 연역

발생상황

1. template instantiation

  • 템플릿을 인스턴스화 하는 과정에서 레퍼런스에 대한 레퍼런스가 나타나면

2. auto 연역

  • 기본적 규칙은 1번과 같음
1
2
3
4
Widget widgetFactory();
Widget w;
auto&& w1 = w; //Widget&
auto&& w2 = widgetFactory(); // Widget&&
  • 보편참조라고 부를 수 있는 경우
    • 타입 연역이 lvalue, rvalue를 구분할 수 있을 때
      • 타입 T의 lvalue가 T&로 , rvalue가 T로 추론되는 경우
    • 참조축약이 일어날 때

3. typedef 또는 alias declaration을 쓸 때

1
2
3
4
5
template<typename T>
class Widget{
  public:
    typedef T&& RvalueReftoT;
}
  • Widget을 왼값참조` 타입을 이용해 인스턴스화한 경우
1
2
Widget<int&> w;
typedef int& && RvalueRefToT; // Widget<int&>::RvalueRefTot

4. decltype

  • decltype 처리중 레퍼런스에 대한 레퍼런스가 나타나는 경우
1
2
3
4
5
int& func(int k);

decltype(func(3))& t; // decltype(func(3)) -> int&
                      // int& & -> int&
                      // int& t;

항목29: 이동 연산이 존재하지 않고, 저렴하지 않고, 적용되지 않는다고 가정하라


  • 이러한 가정은 template 등을 짤 때에 해당함

    • 어떤 타입이 오는지 모르기 때문
  • move 미지원 타입

    • move를 사용한다해도 성능 상의 이득이 없을 가능성이 있음
      • 이동연산이 미구현되어있으면, 복사 연산을 수행하기 때문

std::vector 와 std::array 차이

std::vector

1
2
std::vector<Widget> vw1;
auto vw2 = std::move(vw1);
  • vw1을 vw2로 move하는 건 상수 시간에 가능.
  • vector
    • 내부적으로 데이터를 힙 공간에 할당 후, 그 영역에 대한 포인터를 관리하기 때문
    • move는 그저 포인터를 가리키게하면 끝

std::array

1
2
3
std::array<Widget, 10000> aw1;

auto aw2 = std::move(aw1);
  • aw1을 aw2로 move하는 건 선형 시간

    • aw1의 모든 원소를 aw2로 move시켜야 함.
  • array

    • 힙에서관리하는게 아니라 컨테이너 내부에 직접 관리
    • 원소 각각에 대해 move 연산을 수행

std::string

  • 상수시간의 move와 상수시간의 copy

    • but, 고효율이아님
  • SSO(small string optimization)

    • std::string은 보통 15글자 안쪽 정도의 짧은 문자열의 경우 힙에 할당 x (내부 버퍼에 저장)
      • copy가 더 빠를 가능성이 높음

이동이 아니라 복사가 일어나는 상황`

  • noexcept는 표준의 몇몇 컨테이너들이 강한 예외 안정성을 보장함
    • 이 보장에 의거한 C++98코드는 C++11로 업그레이드했을 때 깨지지 않아야함
      • move 연산이 어떤 예외도 던지지 않을 때에만 copy 연산이 move 연산으로 바뀔 수 있음
    • move가 noexcept로 선언되지 않았다면, copy연산을 수행할 가능성이 있음

요약

  • move가 별로일 때
  • move 연산 제공하지 않을 때 copy
  • move가 더 빠르지 않은 경우 (몇몇 객체들은 copy연산이 더 효율적일 수 있음)
  • move를 사용할 수 없는 경우 (강한 예외 안정성 보장)

  • 별로 효율적이지 않은 또 다른 경우
    • source object가 lvalue일 때 (항목25{move, forward}를 제외하고, move 연산의 source object는 반드시 rvalue)

항목30: 완벽 전달이 실패하는 경우들을 잘 알아두라


  • 완벽전달
    • 하나의 함수에서 다른 함수로 인자들을 완벽하게 (lvalue, rvalue, const, volatile) 전달
    • But, 실패하는 경우가 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
//원소 하나 전달
template<typename T>
void fwd(T&& param)
{
    f(std::forward<T>(param));
}

//임의 개수 원소 전달
template<typename... Ts>
void fwd(Ts&&... params)
{
    f(std::forward<Ts>(params)...);
}
  • 위와 같이 우회함수가 있을 경우 완벽전달이 실패하는 경우가 있음

중괄호 초기화(Braced initializers)

1
2
3
4
5
6
7
void f(const std::vector<int>& v);

// {1, 2, 3}이 암시적으로 std::vector<int> 로 변환
f({1, 2, 3});

// 컴파일 실패
fwd({1, 2, 3});
  • fwd({1, 2, 3})

    • perfect forwarding에 실패
    • 암시적 타입변환이 일어나지 않기 때문
    • 중괄호 초기화가 std::initizlizer_list로 선언되지 않았기 때문
      • 하지만, auto 는 std::initizlizer_list로 추론해냄
      • auto il = {1, 2, 3}; fwd(il);
  • 컴파일러는 간접적으로 호출된 장소에서 넘어온 인자와 f의 매개변수를 서로 비교하지 않음

    • 대신, fwd의 인자 타입을 추론

perfect forwarding이 실패하는 경우

컴파일러가 타입을 추론할 수 없는 경우

  • fwd의 매개변수 중 일부의 타입을 추론할 수 없는 경우
  • 컴파일 실패

컴파일러가 잘못된 타입을 추론하는 경우

  • fwd의 매개변수 중 일부의 타입을 잘못 추론하는 경우
    • 잘못 추론된 타입으로 인스턴스화된 함수 템플릿이 컴파일 될 수 없거나 fwd에서 추론한 타입을 이용한 함수 f의 호출이 직접 f를 호출하는 것과 다른 결과를 내는 경우
    • 만약 f 가 오버로딩된 함수라면, fwd에서 추론을 잘못했을 때 이상한 오버로딩 함수가 호출될 가능성이 있음

0이나 NULL을 null pointer로 쓸 때

  • 정수 타입으로 연역하기 때문 (항목8)

선언만 된 static const 데이터 멤버

  • static const 정수 데이터는 선언만 해도 됨 (컴파일러는 이에 대한 메모리 공간을 따로 할당 x)
1
2
3
4
5
6
7
8
9
10
class Widget
{
public:
    static const std::size_t MinVals = 28;
    ...
};
...

std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals);
  • MinVals를 사용해서 capacity 값을 설정
    • 컴파일러는 정의가 없는 static const 값의 경우 매크로처럼 해당 값을 그 자리에 채워넣음
    • 이 상황에서 MinVals 의 주소값을 취하는 연산을 한다면, 링크타임 에러를 일으킴
1
2
3
4
void f(std::size_t val);

f(Widget::MinVals); //  f(28)
fwd(Widget::MinVals); // 링크 에러 발생
  • fwd 가 인자로 받는게 레퍼런스이기에 에러

    • 레퍼런스는 대부분 내부적으로 포인터와 동일하게 취급
      • 바이너리 코드에서는 동일하게 포인터처럼 취급
      • 사용할 때만 자동으로 역참조 연산을 수행해주는 형태로 구현하는 것이 레퍼런스
      • 실질적으로 포인터와 별 차이가 없음
  • 해결: cpp 파일에 정의

Overload function name, template name

1
2
3
4
5
6
7
void f(int (*pf)(int));

//참고 : 이것도 같은 의미.(non-pointer syntax)
void f(int pf(int));

int processVal(int value);
int processVal(int value, int priority);
  • f는 함수를 인자로 받아 처리하는 함수

  • f(processVal)
    • f는 첫번째 오버로드 함수가 적합한 것을 알 수 있고, 그 주소를 넘김
  • fwd(processVal)

    • 오버로드된 함수중 뭘 선택해야할 지 모름
  • processVal은 이름만 가지고 타입이 없기 때문

    • 타입이 없음 == 타입 연역 불가
  • 템플릿에서도 같은 문제 발생
    • 인스턴스화 해야할 방법이 없기 때문
1
2
3
4
5
template<typename T>
T workOnVal(T param)
{ ... }

fwd(workOnVal); //에러!
  • 해결방법: 지역변수 사용
1
2
3
4
5
6
using ProcessFuncType = int(*)(int);

ProcessFuncType processValPtr = processVal;

fwd(processValPtr);
fwd(static_cast<ProcessFuncType>(workOnVal));

Bitfields

  • 비트필드를 함수의 인자로 받는 경우
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct IPv4Header
{
    std::uint32_t version:4,
                  IHL:4,
                  DSCP:6,
                  ECN:2,
                  totalLength:16;
};

void f(std::size_t sz);

IPv4Header h;

f(h.totalLength);
fwd(h.totalLength); // 오류
  • fwd는 인자로 레퍼런스를 받음

    • h.totalLength는 const가 아닌 비트필드이기 때문에 발생하는 문제
  • C++ 표준에서 const가 아닌 레퍼런스는 bit field와 바운드 될 수 없다라고 명시됨

    • bitfield는 machine의 워드 크기의 일정 부분(32비트 int의 3-5비트와 같이)으로 구성될 수 있음
    • 이걸 직접 엑세스할 수 있는 방법은 없음
    • 포인터나 레퍼런스나 내부적으로는 같음.
      • 워드를 구성하는 작은 일부 비트 위치를 가리키는 주소 같은 건 존재할 수가 없기 때문이다.
      • 단, 비트필드를 값으로 전달하거나 const reference로 전달하는 건 가능
      • const reference를 쓸 경우 컴파일러는 해당 비트필드의 복사본을 만든 후 그 복사본이 저장된 타입의 레퍼런스를 쓰는 것
  • perfect forwarding 함수에 비트필드를 넘기고 싶은 경우에

    • 다른 곳에 저장한 다음(복사) 그걸 인자로 넘겨주면 됨
1
2
auto length = static_cast<std::uint16_t>(h.totalLength);
fwd(length);

참고

  • https://jwvg0425.tistory.com
This post is licensed under CC BY 4.0 by the author.

[C++] emc++ 03: Smart Pointer

[C++] emc++ 04: Lambda