서론
현재 다니고 있는 직장은 c++로 된 프로젝트가 메인 프로젝트 입니다. 몇 년간 쌓여온 레거시가 c 스타일의 코드들로 되어있는데, 최근 리팩토링 요구가 있어 팀원들의 c++ 이해도를 높이기 위한 팀내 세미나를 진행하고 있습니다. 팀내 세미나 진행 내용을 블로그에도 적어두려고 합니다.
우선적으로 기존 코드에 많이 산재되어있는 c 스타일의 raw 포인터를 대체할 수 있는 방법에 대한 세미나를 진행하였고, 첫 세미나 시간은 c++ 레퍼런스 문법에 대해 진행하였습니다.
포인터 최소화 - reference와 const
c++ 레퍼런스는 c의 포인터를 대체하기 위한 문법입니다. 특히 함수의 입력 파라메터 call by reference를 기존 c 스타일 raw 포인터를 사용하는것보다 쉽게 사용할수있습니다. 포인터를 레퍼런스로 대체하면 포인터의 많은 문제점을 최소화 하고, 쉽고 간결한 코드를 작성 할 수 있게 해줍니다.
또한 함수의 입력파라메터에 reference와 const를 조합하여 사용하면 코드에 의미를 내포하게 되어 보기 쉬워지고 컴파일러 최적화에 유리해집니다.
이 글을 보기 전에 알아야 하는 지식
- call by reference와 call by value의 차이
- struct
- c 포인터 사용방법
- const
레퍼런스 사용
아래 코드는 포인터와 레퍼런스를 이용한 동일한 연산을 하는 코드입니다.
int x = 10;
int* p = &x;
*p = 20; // x의 값이 20으로 변경됩니다.
int y = 10;
int& r = y;
r = 20; // x의 값이 20으로 변경됩니다.
레퍼런스의 특징
- 포인터 역참조 연산자 같은 별도의 연산자를 사용하지 않아도 됩니다.
- 초기화 뒤 레퍼런스를 변경할 수 없습니다 (const type * 와 동일)
연산자를 별도로 사용하지 않아도 되며, 초기화 뒤에 변경할 수 없기 때문에 휴먼 에러를 줄이는데 도움이 됩니다. 기존 포인터를 사용할때 역참조 연산자를 빠트리고 코드를 작성했다면, 참조해야할 주소자체가 변해버리는 실수가 생길 수 있습니다. 레퍼런스의 경우 별도의 연산자를 사용하지 않으며, 레퍼런스 대상이 바뀌지도 않기 때문에 raw 포인터를 사용할때보다 안전한 코드를 작성할수있습니다.
함수에서 레퍼런스 사용
레퍼런스는 함수의 인자로 사용될 때 특히 유용합니다. 포인터를 인자로 사용하면 함수 내부에서 null 검사를 해야 하고 역참조 연산자를 써야 하지만, 레퍼런스를 사용하면 그런 부분을 걱정할 필요가 없습니다.
아래 코드의 두 함수는 동일한 기능을 합니다:
void updateValue(int* p) {
if (p != NULL) {
return;
}
*p = 20;
}
void updateValue(int& r) {
r = 20;
}
int main(){
int x = 10;
int y = 10;
updateValue(x);
updateValue(y);
// 20 20 출력됨
std::cout << x << " " << y << std::endl;
}
레퍼런스를 사용 해야 할 때
기본적으로 포인터를 call by reference로 사용할때 대체 할 수 있습니다. 특히 아래와 같은 경우 더욱 유용합니다.
- 함수의 입력으로 큰 구조체를 전달할때
- 함수 내에서 여러개의 값을 반환해야하거나 함수 외부의 값을 바꿔야할때
기존 raw 포인터를 사용하던 다른 이유인 동적할당의 경우, 레퍼런스가 아닌 다음에 다룰 스마트 포인터를 사용하여 대체합니다.
사용 예제
void get_undistorted_image(const cv::Mat& input_image,
cv::Mat& output_image,
const cv::Mat& intrinsic_mat,
const std::array<float,5>& distortion_params){
//do somthing
}
위 코드는 이미지와 각종 파라메터를 받아 이미지에 변형을 가하고 다른 이미지를 반환하는 코드입니다. 입력으로 수백 킬로 바이트의 이미지와 몇가지 파라메터와 행렬을 받고, 출력으로 이미지를 리턴해야 합니다.
- 위 코드에서 call by value를 사용한다면 객체를 새로 생성하고 메모리영역을 새로 할당해야 합니다.
- std::array등의 경우 전체가 복사됩니다.
- cv::Mat의 경우 기본 복사 동작이 shallow copy이지만 객체가 새로 생성되고 버퍼 외 나머지 객체에 대한 정보값들은 메모리를 새로 할당하게 됩니다.
- 위 코드에서 Call by reference를 위해 포인터를 사용하는경우 nullptr여부를 무조껀 체크해야 하며 포인터 역참조 연산자를 잊어버리는 경우 같은 실수가 발생 할 수 있습니다.
call by reference를 c++ 참조 연산자를 통해 구현하면 실수를 줄이고 성능적으로도 문제 없이 사용할 수 있습니다.
const 사용
레퍼런스와 const를 적극적으로 사용하면 매개 변수에 입력과 출력 의미를 포함시켜서 더 깔끔한 코드를 만들수있습니다.
void draw_circle(cv::Mat& image,
const cv::Point& center,
const int radius){
// do something
}
void draw_circle(const cv::Mat& input_image,
cv::Mat& output_image,
const cv::Point& center,
const int radius){
// do something
}
int main(){
cv::Mat input_image = cv::imread("asdf.jpg")
//입력받은 input_image에 원이 그려지는 함수입니다
draw_circle(input_image, cv::Point(200,300), 5);
cv::Mat output_image;
//output_image에 원이 그려진 이미지가 출력되는 함수입니다
draw_circle(input_image, output_image, cv::Point(400,500), 5);
}
위 코드를 실행하면 input_image 이미지에는 원이 하나, output_image에는 원이 두개가 그려지게 됩니다.
const를 붙이고 사용하는것을 생활화 하면 이 매개변수가 입력인지, 입출력인지를 직관적으로 나타낼수 있습니다. const가 붙어있는 매개변수는 함수 내부에서 해당 객체의 값이 변하는 행위가 불가능해지므로, 함수를 사용하는 입장에서 함수 내부 코드를 볼 필요 없이 매개 변수만 보는 것으로도 코드를 짜는것을 도울 수 있습니다.
위 코드에서는 메모리 최적화를 위해 입력 메모리에 중복해서 원을 그리기를 원한다면 input만 있는 함수를 쓰면 되고, 원본데이터가 변하지 않기를 원한다면 명시적으로 output_image를 매개변수로 받으면서 input_image가 const로 되어있는 함수를 사용하면 됩니다.
주의할점
- 레퍼런스는 다중스레드에 대응하기 힘듭니다.
- 레퍼런스한 원본 변수의 메모리가 해제되면 레퍼런스 변수가 유효하지 않게 됩니다.
따라서 코드에서 레퍼런스를 적극적으로 사용하는것보단, 함수의 매개변수의 포인터변수를 레퍼런스 변수로 변경하는 정도의 활용이 적합함
첨언 - 잘못된 사용
잘못된 사용
일반적으로 함수의 리턴을 외부에서 참조로 받거나, 함수의 리턴값을 참조로 하거나, 둘다 하는 행위는 지양해야합니다.
// 일반적인 함수
cv::Mat someFunction_return_local_lvalue() {
cv::Mat localVariable(100,100);
return localVariable;
}
// 일반적인 함수
void someFunction_void_return(cv::Mat& output) {
output(100,100);
return;
}
// 이렇게 쓰지 마세요 - 로컬변수를 레퍼런스로 리턴하면 메모리가 해제됩니다!
cv::Mat& someFunction_return_local_lvalue_reference() {
cv::Mat localVariable(100,100);
return localVariable;
}
// 그냥 그래요 - 왠만하면 리턴으로 레퍼런스를 사용하지 마세요. 코드가 혼란스러워 집니다.
cv::Mat& someFunction_return_global_value_reference(cv::Mat& input) {
input = cv::Mat(100,100);
return input;
}
int main() {
// -- 좋은 사용
// 이와 같은 경우엔 값이 복사될것 같지만, 컴파일러가 최적화를 하여 로컬변수를 복사하지 않고 이동 처리 하기때문에 효율적입니다.
cv::Mat value = someFunction_return_local_lvalue();
// call by reference로 출력을 받으므로 효율적입니다.
cv::Mat x;
someFunction_void_return(x);
// -- 그냥 그런 사용
// value1은 a가 복사 되며 값은 같으나 다른객체입니다.
cv::Mat a;
cv::Mat value1 = someFunction_return_global_value_reference(a);
// a와 value2는 동일한 객체입니다.
cv::Mat& value2 = someFunction_return_global_value_reference(a);
// -- 잘못된 사용
// 둘다 함수의 로컬변수의 레퍼런스를 받으므로 이미 해제된 메모리를 받게 됩니다.
cv::Mat& ref1 = someFunction_return_local_lvalue();
cv::Mat& ref2 = someFunction_return_local_lvalue_reference;
}
레퍼런스는 아래 코드처럼 initialize 없이 사용하면 컴파일 에러가 발생합니다
// compile error!
int& reference_without_initialize;