c++ 참조자 질문

ghoflvhxj의 이미지

#include <iostream>
 
int main()
{
	int a = 10;
	const int &ref = a;
 
	int *p = &a;
	const int *p2 = p; 
	const int *(&pref) = p;
 
	system("PAUSE");
	return 0;
}

사진에서 볼 수 있듯이
포인터 참조자 pref의 값을 초기화를 할 수 없던데 왜 그럴까요?
const int &ref를 a로 값을 초기화 했던건 됐으면서 왜 안되는지 궁금합니다

File attachments: 
첨부파일 크기
Image icon 캡처.JPG24.2 KB
Image icon 제목 없음.jpg50.81 KB
익명 사용자의 이미지

위 코드에서 ref는 상수에 대한 레퍼런스이지만 pref는 상수에 대한 레퍼런스가 아니기 때문이죠.

무슨 일이 일어나고 있는지 이해하는 데 필요한 걸 다 설명하기엔 설명해야 할 게 너무 많습니다.
일단 아무튼 잘 되는 코드 하나 받고 싶으시다면 그 정도는 해 드릴 수 있겠네요.

#include <iostream>
 
int main()
{
	int a = 10;
	const int &ref = a; // 상수에 대한 레퍼런스. 물론 int &ref였어도 아무 문제 없습니다.
 
	int *p = &a;
	const int *p2 = p; // 이게 되는 건 p에 대한 레퍼런스를 만드는 것과는 조금 다른 문젭니다. a의 주소값이 prvalue로서 qualification conversion을 거쳤을 뿐이죠. 그게 대체 뭐냐고 묻는다면, 설명하기 귀찮아요..
	const int * const & pref1 = p; //이러면 pref1은 상수에 대한 레퍼런스. 헷갈리신다면 포인터는 그 자체가 상수일 수도 있고, 가리키는 대상이 상수일 수도 있다는 점을 상기하시고, 그 문법을 복습해 보시는 게 좋겠습니다.
	int * const & pref2 = p; // 사실 포인터가 가리키는 대상인 int는 꼭 상수일 필요가 없었죠.
	int *&pref3 = p; // 그 경우엔 물론 상수 레퍼런스일 필요도 없습니다.
 
	system("PAUSE");
	return 0;
}

작성자의 이미지

감사합니다. 그런데 괜찮으시면 무슨 일이 일어나는지도 알 수 있을까요

익명 사용자의 이미지

일단 N4659(http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2017/n4659.pdf) 기반으로.

N4659 11.6.3.4 wrote:
Given types "cv1 T1" and "cv2 T2", "cv1 T1" is reference-related to "cv2 T2" if T1 is the same type as T2, or T1 is a base class of T2. "cv1 T1" is reference-compatible with "cv2 T2" if

- T1 is reference-related to T2, or
- T2 is "noexcept function" and T1 is "function", where the function types are otherwise the same,

and cv1 is the same cv-qualification as, or greater cv-qualification than, cv2. In all cases where the reference-related or reference-compatible relationship of two types is used to establish the validity of a reference binding, and T1 is a base class of T2, a program that necessitates such a binding is ill-formed if T1 is an inaccessible (Clause 14) or ambiguous (13.2) base class of T2.

이하 "cv1 T1" 타입에 대한 reference를 "cv2 T2"타입의 expression으로 초기화할 수 있는 조건들이 쭉 나오는데 다 필요하지도 않고 인용하기 귀찮으므로 모두 알고 싶으시면 위 표준 드래프트 문서에서 11.6.3.5를 읽어 나가시면 됩니다. 여기선 필요한 만큼만 인용하도록 하죠.

const int *(&pref) = p;

레퍼런스는 lvalue reference이고, 대상 타입은 "cv1 T1" = "const int *"이므로 cv1 = (none), T1 = "const int *"
표현식 p는 lvalue이고 타입은 "cv2 T2" = "int *"이므로 cv2 = (none), T2 = "int *"

11.6.3.5 조건 체크:
(5.1) lvalue reference인가? 예.
(5.1.1) expression이 lvalue인가? 예. "cv1 T1"이 "cv2 T2"에 reference-compatible한가? 아니오. (const int *는 int *에 reference-related 하지 않음)
(5.1.2) expression이 class type인가? (i.e., T2가 class type인가?) 아니오.
(5.2) 레퍼런스가 non-volatile const type에 대한 lvalue reference이거나 (즉 cv1가 "const"), rvalue reference인가? 아니오.

체크 끝. 모든 조건이 실패하므로 const int *(&pref) = p;은 컴파일 되지 않습니다.

재미삼아 const int &ref = a;는 왜 되는지 볼까요? 마찬가지로...

레퍼런스는 lvalue reference이고, 대상 타입은 "cv1 T1" = "const int"이므로 cv1 = "const", T1 = "int"
표현식 a는 lvalue이고 타입은 "cv2 T2" = "int"이므로 cv2 = (none), T2 = "int"

11.6.3.5 조건 체크:
(5.1) lvalue reference인가? 예.
(5.1.1) expression이 lvalue인가? 예. "cv1 T1"이 "cv2 T2"에 reference-compatible한가? 예. (T1 = T2 = "int"이므로 T1은 T2에 reference-related이고, cv1 = const > cv2 = (none)으로 cv1은 cv2보다 greater cv-qualification이므로.)

체크 끝. 이 경우 ref는 a에 묶입니다. (reference binding)

const int *p2 = p;는 앞에서 말씀드렸듯 전혀 다른 문젭니다. 이건 레퍼런스 초기화가 아니라 그냥 포인터 변수 초기화죠. p의 값(a의 주소. 타입 int *)이 qualification conversion을 거쳐 const int *이 되고, 그 값으로 p2를 초기화하는 겁니다.

이상 C++17 표준 드래프트를 바탕으로 각 초기화가 어떻게 허용되고 어떻게 금지되는지 설명이 된 것 같군요.

하지만 왜?를 묻는다면 이야기가 조금 달라집니다. C++언어 표준은 마치 프로그램과도 같아서 뭔가를 할 수 있는 절차는 잘 알려주지만, 그 Rationale을 친절히 설명해 주진 않거든요. 그러니 이 아래부터는 제 주관적인 해석입니다.

non-constant lvalue reference에 lvalue를 직접 묶는다는 것은 reference가 대상 lvalue의 alias가 되는 셈입니다. 특히나 레퍼런스만 가지고 원본 lvalue를 덮어쓸 수 있기 때문에 문제가 되는데요. 이런 경우를 생각해보시죠.

#include <iostream>
 
int main()
{
	char a[] = "hello"; // 수정 가능한 문자 배열 a
	char *p = a; // 배열 a의 첫 요소를 가리킴 (가리키는 문자는 수정 가능)
	const char *p2 = "world"; // 수정 불가능한 string literal을 가리키는 포인터 p2. (가리키는 문자는 수정 불가능)
 
	const char *&pref = p; // 앞서의 예에서 int 대신 char를 사용함. 원래 컴파일이 안 되지만, 만약 된다면 pref는 p를 참조해야겠죠.
	pref = p2; // pref는 const char *를 가리키는 non-constant lvalue reference이고, 즉 p를 가리키는 modifiable lvalue가 됩니다. 이걸 p2로 덮어쓴다면, p에 p2의 값이 덮어쓰이는 거죠.
	p[0] = '@'; // p는 (pref에 의해 간접적으로 덮어쓰여져) 현재 string literal "world"를 가리키고 있습니다. 하지만 char * 타입이죠. 이런 식으로 덮어쓰는 걸 타입 수준에서 막을 수 있는 방법이 없어요.
	// 처음부터 p = p2;를 시도했으면 const char *를 char *로 바꿀 방법이 없으므로 강제로 캐스팅하지 않는 한 컴파일이 안 됐겠죠.
	return 0;
}

위와 같은 이상한 짓을 강제 타입 캐스팅 없이 시도할 수 있다는 것은 뭔가 문제가 있다는 뜻이지요. 저는 바로 이런 경우를 막기 위해서 pref와 같은 reference 초기화를 금지했을 거라고 생각합니다.

ghoflvhxj의 이미지

자세한 설명 감사드려요.
어렵고 복잡하군요 껄껄