💬 지난 글에 이어 const를 쓰면서 막히는 사례를 풀어가 보자.
애증의 const 사용하기 (1)
const 잘 쓰시나요?💬 나는 잘 사용하지 않는다. const를 사용하다 보면 에러가 자주 발생하고, 개발하는데 적지 않은 인터럽트가 걸린다. 이런 경험들이 반복되면서 자연스럽게 const 사용하지 않
jabdon4ny.tistory.com
기존에 작성되어 있던 used defined type 객체를 멤버함수의 인자로 전달하는 경우를 생각해 보자.
예제
Point 객체를 foo 함수 인자에 상수 참조형으로 전달을 한 예제인데 멤버함수를 호출하면 에러가 발생한다.
#include <iostream>
class Point {
public:
int x, y;
int getX() {return x;};
int getY() {return y;};
void setX(int value) {x = value;};
};
void foo(const Point& p){
p.getX(); // error
}
int main() {
return 0;
}
💬 그러면 아래와 같은 흐름으로 이어진다.
'아 const 삭제하고 싶다.' 하지만 const를 한 번 제대로 써보려고 마음먹었기에 다시 에러 코드를 본다.
<source>:13:11: error: passing 'const Point' as 'this' argument discards qualifiers [-fpermissive]
상수 멤버 함수가 아니라는 이유로 에러가 뜨니 함수에 const를 붙여 상수 멤버함수로 만들자며 무지성으로 const를 붙이기 시작한다.
#include <iostream>
class Point {
public:
int x, y;
int getX() const {return x;};
int getY() const {return y;};
void setX(int value) const {x = value;}; //error
};
void foo(const Point& p){
p.getX();
}
다시 컴파일!
source>:8:35: error: assignment of member 'Point::x' in read-only object
멤버변수도 상수로 처리되는 상황에 이르자 const를 전부 지워 깔끔하게(?) 해결한다.
//코드에서 const 가 사라졌다.
#include <iostream>
class Point {
public:
int x, y;
int getX() {return x;};
int getY() {return y;};
void setX(int value) {x = value;};
};
void foo( Point& p){
p.getX();
}
int main() {
return 0;
}
💬 황당한 전개라고 생각할 수 있는데 실제 경험담이다. 이럼에도 문제가 없었던 이유는 동료가 리뷰할 때 const 사용유무 자체를 리뷰하지 않으며, 이 영역이 Black box이기에 고객은 모른다.
그렇다면 이 문제를 해결하려면 어떻게 해야 할까? 🤔
const 버전, non-const 버전을 동시에 제공
멤버 함수를 const와 non-const 버전을 동시에 제공하면 된다. 두 버전을 제공하면 어떻게 되는지 아래 예제를 통해 그 동작을 이해해 보자.
예제
아래 예제는 동일한 이름의 non-const 함수(1번)와 const 함수(2번)를 제공하고 그 함수를 호출하는 프로그램 코드이다.
#include <iostream>
class Test
{
public:
void foo() { std::cout << "foo\n"; } // 1
void foo() const { std::cout << "foo const\n"; } // 2
};
int main()
{
Test t;
const Test ct;
t.foo();
ct.foo();
}
user defined type 객체인 Test를 non-const와 const 객체로 생성한 뒤 foo 함수를 호출했을 때 동작결과를 확인해 보면
t.foo() 호출 시
1번 non-const foo()가 우선 호출이 되지만 non-const foo() 함수가 없어도 2번 const foo()로 호출이 된다.
ct.foo() 호출 시
상수 객체이므로 상수 멤버함수만 가능하기에 2번 const foo() 함수만 호출이 가능한다.
이렇게 두 가지 버전을 제공하면 타입에 맞는 함수가 호출이 된다.
표준 내장 클래스에서도 많이 사용
표준 제공 클래스에서도 동시에 const와 non-const 함수를 제공하기에 한번 확인해 보자.
std::vector를 const와 non-const 객체로 생성하여 멤버함수를 호출해 보자.
#include <vector>
int main()
{
std::vector<int> v1 = { 1,2,3 };
const std::vector<int> v2 = { 1,2,3 };
auto p1 = v1.begin(); // vector 반복자 => iterator 반환
auto p2 = v2.begin(); // const vector 반복자 => const_iterator 반환
}
만약 const begin() 함수를 제공하지 않았다면 v2.begin() 코드에서 에러를 발생시켜야 하지만 발생하지 않음을 확인할 수 있다.
begin() 함수가 정의된 부분을 보면 아래와 같이 const 함수와 non-const 함수를 둘 다 제공하고 있다. (iterator는 아예 클래스도 const와 non-const로 나눠놨다.)
_NODISCARD _CONSTEXPR20 iterator begin() noexcept {
auto& _My_data = _Mypair._Myval2;
return iterator(_My_data._Myfirst, _STD addressof(_My_data));
}
_NODISCARD _CONSTEXPR20 const_iterator begin() const noexcept {
auto& _My_data = _Mypair._Myval2;
return const_iterator(_My_data._Myfirst, _STD addressof(_My_data));
}
[ ] 연산자의 const, non-const 제공
[] 연산자의 경우, 표준에 정의되어 있는 클래스에서 const와 non-const 버전 함수, 둘 다를 제공하고 있는 것을 심심치 않게 볼 수 있다. 왜 이렇게 제공하고 있는지 그 이유를 확인해 보자.
_NODISCARD _CONSTEXPR20 _Ty& operator[](const size_type _Pos) noexcept /* strengthened */ {
auto& _My_data = _Mypair._Myval2;
#if _CONTAINER_DEBUG_LEVEL > 0
_STL_VERIFY(
_Pos < static_cast<size_type>(_My_data._Mylast - _My_data._Myfirst), "vector subscript out of range");
#endif // _CONTAINER_DEBUG_LEVEL > 0
return _My_data._Myfirst[_Pos];
}
_NODISCARD _CONSTEXPR20 const _Ty& operator[](const size_type _Pos) const noexcept /* strengthened */ {
auto& _My_data = _Mypair._Myval2;
#if _CONTAINER_DEBUG_LEVEL > 0
_STL_VERIFY(
_Pos < static_cast<size_type>(_My_data._Mylast - _My_data._Myfirst), "vector subscript out of range");
#endif // _CONTAINER_DEBUG_LEVEL > 0
return _My_data._Myfirst[_Pos];
}
우선 아래 예제는 std::vector 클래스를 약식으로 만든 미완성된 코드이며 operator []를 정의해 보자.
#include <iostream>
template<typename T> class vector
{
T* buff;
int size;
public:
vector(int sz) : buff(new T[sz]), size(sz) {}
~vector() { delete[] buff; }
//<return type> operator[](int idx) { return buff[idx]; }
};
int main()
{
vector<int> v(10);
v[0] = 0; // error
}
<return type> 어떻게 정의해야 할까? 🤔
T operator[](int idx) { return buff[idx]; }
T 반환 : 이렇게 되면 buff[idx]가 가진 값을 반환한 r-value이기에 '0'을 대입할 수 없다.
T& operator[](int idx) { return buff[idx]; }
T&반환 : buff[idx] 위치의 별명을 반환하기에 대입이 가능하다.( 실제는 'v[0] = 0' 이와 같은 코드가 된다.)
기본형은 준비되었다. 그럼 이제 const, non-const로 생성된 vector객체를 배열로 사용할 때 동작을 확인해 보자.
#include <iostream>
template<typename T> class vector
{
T* buff;
int size;
public:
vector(int sz) : buff(new T[sz]), size(sz) {}
~vector() { delete[] buff; }
T& operator[](int idx) { return buff[idx]; }
};
int main()
{
int n1 = 0, n2 =0;
vector<int> v1(10);
const vector<int> v2(10);
// // 원하는 동작 실제 결과
v1[0] = 0; // O O
n1 = v1[0]; // O O
v2[0] = 0; // X X
n2 = v2[0]; // O X
}
const 객체에 배열 사용 시 동작결과가 맞고 틀리고를 떠나서 성공, 실패의 원인을 확인해봐야 한다.
결과의 이유 | |
v2[0] = 0; | 원하는 동작 : 상수 객체이기에 대입은 안되어야 한다. 실제 결과 : 실제로 에러가 난 이유는 operator[]에 const 멤버 함수가 없어서이다. |
n2 = v2[0]; | 원하는 동작 : 상수 객체이지만 상수 객체의 값을 변수에 대입하는 것은 가능하다. 실제 결과 : 대입 이전에 상수 멤버함수를 제공하지 않아서 사용하는 시점에 에러가 발생했다. |
이런 오동작 현상을 바로잡기 위해 const 멤버함수로 변경해서 결과를 다시 확인해 보자.
(일반 객체에서는 const 멤버함수도 호출되기 때문에 불필요한 코드 낭비를 막기 위해 const 함수만 제공해 보자.)
#include <iostream>
template<typename T> class vector
{
T* buff;
int size;
public:
vector(int sz) : buff(new T[sz]), size(sz) {}
~vector() { delete[] buff; }
T& operator[](int idx) const { return buff[idx]; }
};
int main()
{
int n1 = 0;
vector<int> v1(10);
const vector<int> v2(10);
// // 원하는 동작 실제 결과
v1[0] = 0; // O O
n1 = v1[0]; // O O
v2[0] = 0; // X O
n2 = v2[0]; // O O
}
다시 동작 결과를 확인해 보자.
결과의 이유 | |
v2[0] = 0; | 원하는 동작 : const 객체이기에 대입은 안되어야 한다. 실제 결과 : const는 buff의 포인터까지만 수정이 불가하고 buff의 data는 const가 아니기게 대입이 가능하다. |
n2 = v2[0]; | 원하는 동작/실제 결과 : 상수 객체이지만 상수 객체의 값을 변수에 대입하는 것은 가능하다. |
이번에는 const 객체를 대입이 불가하게 하기 위해 아래와 같이 리턴 값에 const를 추가를 하자.
const T& operator[](int idx) const { return buff[idx]; }
또 문제가 있다. 이렇게 하면 반대로 non-const 객체에 대한 대입이 불가해진다. ( 'v1[0] = 0' // error ) 그래서 const 함수만으로는 해결이 안 되고
non-const operator [] 함수를 추가해야만 완성이 된다. 완성된 코드는 아래와 같다.
완성된 예제 코드
#include <iostream>
template<typename T> class vector
{
T* buff;
int size;
public:
vector(int sz) : buff(new T[sz]), size(sz) {}
~vector() { delete[] buff; }
T& operator[](int idx) { return buff[idx]; }
const T& operator[](int idx) const { return buff[idx]; }
};
int main()
{
int n1 = 0, n2 = 0;
vector<int> v1(10);
const vector<int> v2(10);
v1[0] = 0;
n1 = v1[0];
//v2[0] = 0;
n2 = v2[0];
}
완성된 코드를 통해 왜 표준 제공 클래스에 operator [] 함수를 const와 non-const 버전 둘다를 제공하고 있는지를 확인하였다.
💬 우리가 표준 제공 클래스를 사용했을 때 const 타입으로 인해 컴파일 에러를 경험해보지 못한 것처럼 두 버전을 제공하는 하는 것은 널리 사용하고 있는 방법이기에, 메모리를 절약하고 오동작 가능성을 낮추는 애증의 const를 잘 다뤄 봤으면 한다.
explicit object parameter(deducing this) 기술 소개
C++23
💬 C++은 간소화와 성능 그리고 효율을 중시하는 언어이지만, C++23 이전까지는 (const 키워드의 위치가 함수 인자 바깥에 있어) 간소화가 불가하여 동일한 함수 const, non-const 버전을 둘 다 제공해야 했다.
📜 하지만 C++23의 변화중 첫 번째로 꼽히는 기술은 explicit object parameter(deducing this)이다. 함수인자로 this를 명시적으로 기재할 수 있게 되면서 간소화의 길이 열였다.
우선 아래 코드로 implicit object parameter와 explicit object parameter를 비교해 보자.
class Object
{
int value;
public:
// => 컴파일러는 아래와 같이 코드를 변경한다.
// => "implicit object parameter" 인 this 전달
void f1(int n) // void f1(Object* this, int n)
{
value = n; // this->value = n;
}
// explicit object parameter since C++23
void f2(this Object& self, int n)
{
self.value = n;
}
};
int main()
{
Object obj;
obj.f1(10);
obj.f2(10);
}
이것으로 인해 const 키워드가 함수 인자에 들어가게 된다. 이것이 인자 안에 들어가면 const, non-const 함수를 하나의 템플릿으로 자동생성 할 수 있다. 아래 예제를 참고해 보자.
#include <iostream>
class Test
{
public:
//C++23 이전 - implicit object parameter
void foo() { std::cout << "foo\n"; }
void foo() const { std::cout << "foo const\n"; }
//C++23 이후 - explicit object parameter 지원
void foo2(this Test& self) { std::cout << "foo2\n"; }
void foo2(this const Test& self) { std::cout << "foo2 const\n"; }
//C++23 이후 : const,non-const 버전의 함수를 자동 생성할 수 있다.
//그래서 별명이 deducing this로 this가 자동 추론된다는 의미이다.
template<typename T>
void foo3(this T& self) { std::cout << "foo3 or const foo3 \n"; }
};
int main()
{
Test t;
const Test ct;
t.foo();
ct.foo();
t.foo2();
ct.foo2();
t.foo3();
ct.foo3();
}
기존 foo() 함수가 C++23부터 foo2() 함수로 변경이 가능한데, 이런 형태는 template를 써서 foo3()처럼 하나의 함수로 const와 non-const 버전을 자동생성되게 할 수 있다.
💬 C++23 이전버전을 사용하는 사람들에게는 const를 더 잘 다룰 수 있기를, C++23 버전을 쓰는 사람에게는 멤버함수가 더욱더 template를 써야 하는 배경을 이해할 수 있는 계기가 되었길 바란다.
참고
* 본 글에 예제 코드는 코드누리 샘플코드를 활용하였습니다.
C++ 핵심 가이드라인 한글화 프로젝트 링크
- F.16: "입력(in)" 매개변수는 복사 비용이 적게 드는 타입의 경우 값으로 전달하고, 그 외에는 상수 참조형으로 전달하라
C++ Core Guidelines 원문 링크
- F.16: For “in” parameters, pass cheaply-copied types by value and others by reference to const

'L C++' 카테고리의 다른 글
new/delete 동작 원리 이해하기 (4) | 2024.07.22 |
---|---|
내가 설계한 객체에 유효성 기능 지원하기 (2) | 2024.06.09 |
애증의 const 사용하기 (1) (0) | 2024.05.11 |
(응용) 생성자(constructors) 초기화 (16) | 2024.04.21 |
(기초) 생성자(constructors) 초기화 (1) | 2024.04.17 |