- std::move가 모든 것을 이동하지는 않음
- 완벽전달은 완벽하지 않음
- 이동연산이 복사 연산보다 항상 싼 것은 아님
- 기대한 만큼 싸지 않음
- 이동이 유효한 문맥에서 항상 이동 연산이 호출되는 것은 아님
형식&&
형태의 구성체가 항상 오른값 참조를 나타내는 건 아님- 매개변수는 항상 왼값이다.
- 매개변수 타입이 오른쪽 참조인 경우에도 매개변수 자체는 왼값이다.
항목23: std::move와 std::forward를 숙지하라
std::move
는 모든 것을 이동 xstd::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 오른값에 묶는것이 허용되기에 복사생성자가 호출됨
- 이동을 지원할 객체는 const로 선언하지 말아야함
- const 객체에 대한 이동 요청은 복사연산으로 변환됨
- 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의 선언을 완전하게 결정하기 때문
- ex)
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의 복사를 피할 수 있음
- 단 다음 조건에 만족해야함
- 지역 객체의 타입이 반환타입과 같아야함
- 그 지역객체가 바로 함수의 리턴값이어야함
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 사용
- 그러므로 std::remove_reference
보편 참조를 받는 템플릿을 제한
꼬리표 배분의 필수요소: 클라이언트 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, 특수한 경우에는 참조 축약이라는 특정한 규칙에 의해 레퍼런스로 만듦
- 둘 중 하나라도 lvalue ref 면 결과는 lvalue ref
- 그 외의 경우 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로 추론되는 경우
- 참조축약이 일어날 때
- 타입 연역이 lvalue, rvalue를 구분할 수 있을 때
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를 사용한다해도 성능 상의 이득이 없을 가능성이 있음
- 이동연산이 미구현되어있으면, 복사 연산을 수행하기 때문
- 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가 더 빠를 가능성이 높음
- std::string은 보통 15글자 안쪽 정도의 짧은 문자열의 경우 힙에 할당 x (내부 버퍼에 저장)
이동이 아니라 복사가 일어나는 상황`
- noexcept는 표준의 몇몇 컨테이너들이 강한 예외 안정성을 보장함
- 이 보장에 의거한 C++98코드는 C++11로 업그레이드했을 때 깨지지 않아야함
- move 연산이 어떤 예외도 던지지 않을 때에만 copy 연산이 move 연산으로 바뀔 수 있음
- move가 noexcept로 선언되지 않았다면, copy연산을 수행할 가능성이 있음
- 이 보장에 의거한 C++98코드는 C++11로 업그레이드했을 때 깨지지 않아야함
요약
- 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