매개변수( parameter )의 구분
매개변수( parameter )는 함수를 호출하는 쪽에서의 입력 - 이것을 인수( argument )라고 합니다. - 을 함수 내에 전달하는 변수를 말합니다.
이러한 매개변수는 일반적으로 함수 사용자의 입력을 전달하기 위해서만 사용되는데, 이러한 매개변수를 입력 매개변수( in-parameter )라고 합니다.
아래 예문은 입력 매개변수의 예를 보여줍니다.
#include <iostream>
using namespace std;
void printString( const std::string& str ){ // str은 in-parameter
cout << str << '\n';
}
int main(){
std::string str{ "Hello, world!" }; // str은 argument
printString(str);
}
이러한 입력 매개변수는 주로 인수를 값으로 전달( passed by value )하거나, 상수 참조로 전달( passed by const reference )하는 방식으로 사용됩니다.
이와 반대로, 함수 연산의 결과를 함수 사용자에게 전달하기 위해 사용되는 매개변수도 있습니다.
이러한 변수를 반환 매개변수( out-parameter )라고 합니다.
아래 예문은 반환 매개변수의 예를 보여줍니다.
void getInputedValue( int& inputed){ // inputed는 out-parameter
std::scanf("%d", &inputed);
}
int main(){
int inputed;
getInputedValue(inputed); // inputed는 argument
cout << inputed << '\n';
}
이러한 반환 매개변수는 주로 인수를 참조로 전달( passed by reference )하거나, 주소로 전달( passed by address )하는 방식으로 사용됩니다.
반환 매개변수( out-parameter )의 단점
그런데, 이 반환 매개변수는 몇 가지 단점이 있습니다.
먼저, 반환 매개변수라는 것이 자연스럽지 않은 문법이라는 것입니다.
함수를 사용할 때, 입력을 나타내는 매개변수와 출력을 나타내는 반환 값을 구분해서 사용하게 되면 자연스러운 코드를 만들게 됩니다.
그런데, 반환 매개변수를 사용하게 되면 같은 결과를 가져온다 하더라도 코드가 얽히는 것을 느낄 수가 있습니다.
아래의 코드는 반환 값을 사용하는 함수와 반환 매개변수를 사용하는 함수를 보여줍니다.
// 계산 결과를 함수 반환 값을 통해서 전달
int computeValue1(int val){
return val * val;
}
// 계산 값을 out-parameter로 전달
void computeValue2(int val, int& ret){
ret = val * val;
}
이 함수들을 사용하는 코드는 아래와 같습니다.
int main(){
int val1 = computeValue1(5);
std::cout << computeValue1(2) << '\n';
int val2 = 0;
computeValue2(5, val2); // out-parameter를 사용
int val3;
computeValue2(2, val3); // out-parameter를 사용
std::cout << val3 << '\n';
}
위의 코드들을 보면, 이 경우 반환 매개변수의 불편함이 그대로 드러납니다.
첫 번째 줄을 보면, 이 문장은 변수 val1 을 함수의 결과 값으로 초기화하고 있다는 것을 한눈에 알 수 있습니다.
그렇지만, 두 번째 함수 사용법을 보면, 이 함수가 변수 val2 를 초기화하는지, 값 5 와 val2 변수를 사용해서 다른 계산 값을 구하는지 바로 알 수가 없습니다.
그리고, 세 번째 함수 사용법과 같이, 함수의 결과를 출력하려면 먼저 매개변수로 전달할 인자 val3 를 선언해서, 계산 결과를 구하고, 이 결과 값을 출력하는 두 번의 과정을 거쳐야 합니다.
이렇게 되면, 연쇄 함수의 편리성을 조금도 맛볼 수 없게 됩니다.
그리고, 반환 매개변수를 사용하는 함수를 처음 보게 되면, 이 매개변수가 함수의 연산 결과를 돌려줄 것으로 예상하지만, 실제는 그렇지 않은 경우도 있다는 것을 알게 됩니다.
다음 예제는 아래의 CSomething 객체를 반환 매개변수 형태로 전달받는 printName 함수를 보여줍니다.
이 함수의 기능은 객체의 멤버 함수를 통해 객체의 이름을 얻고, 이를 출력하는 것입니다.
class COtherObj{ // int 값을 저장하는 객체
int m_nValue;
public:
int getValue(){
return 10;
}
void setValue(int val){
m_nValue = val;
}
};
class CSomething{
string m_Name;
COtherObj m_test;
public:
explicit CSomething( string_view name) : m_Name( name ){}
void member_func1() {
// do something
}
void member_func2() {
// do something
m_test.setValue(13);
}
string_view getName() {
member_func1();
member_func2();
return m_Name;
}
};
void printName( CSomething& st){ // out-parameter를 사용하는 함수
cout << st.getName() << '\n';
}
int main(){
CSomething st("first object");
printName( st);
}
위의 printName 함수는 반환 매개변수( out-parameter )를 사용하는 함수이고, 이 함수는 매개변수로 전달된 CSomething 객체를 전혀 수정하지 않습니다.
그렇지만, 함수를 들여다보지 않으면, 이 함수가 객체를 변경하지 않는다는 것을 바로 알기는 어렵습니다.
물론, 함수명을 보고 예상은 하지만, "그렇다면 왜 상수 참조 타입( const reference )을 사용하지 않았지?"라는 생각을 떠올리게 됩니다.
왜일까요?
함수 작성자는 printName의 매개변수에 상수 참조 타입을 사용하고 싶었을 것입니다.
그런데, 매개변수의 타입이 상수 속성을 갖도록 변경하는 순간, 다음과 같은 오류를 만나게 됩니다.
error: passing 'const CSomething' as 'this' argument discards qualifiers [-fpermissive]
이렇게 된 이유는, const 객체는 const 멤버 함수만 호출할 수 있기 때문입니다.
그런데, 위의 멤버 함수 getName은 const 멤버 함수가 아닙니다.
이렇게 상수 객체가 const 멤버 함수가 아닌 멤버 함수를 호출하면 만나게 되는 오류 메시지가 위의 메시지입니다.
이를 수정하기 위해서, 아마 다음과 같이 멤버 함수를 변경할 것입니다.
string_view getName() const; // 상수 멤버 함수
그러나, 이렇게 하면 위와 같은 오류 메시지가 오히려 두 개로 늘어나게 됩니다.
getName 안에서 호출된 member_func1과 member_func2 모두 상수 함수가 아니기 때문입니다.
이를 해결하려면, member_func1 함수를 상수 멤버 함수로 변경합니다.
그리고, member_func2 함수도 상수 멤버 함수로 변경해야 하는데, 이렇게 하려는 순간, 벽에 막히게 됩니다.
member_func2 함수 내에서, 멤버 객체 m_test 의 값을 변경하는 setValue 함수를 호출하고 있기 때문입니다.
이렇게, 모든 멤버 함수를 상수화 하는 계획은 여기서 끝입니다.
즉, 일부러 안 한 게 아니라, 그렇게 하기에는 너무 많은 변경사항 문제 때문에 타협을 하게 된 것입니다.
이 경우, 결국은 반환 매개변수( out-parameter )를 사용하게 되겠지만, 이렇게 반환 매개변수를 만능 약처럼 사용하는 것은 일반적으로 코드를 읽고 이해하기 힘들게 만듭니다.
const 멤버 함수에 관한 내용은 여기서 볼 수 있습니다.
반환 매개변수( out-parameter )의 장점
위에서 반환 매개변수가 아주 나쁜 것처럼 말했지만, 적어도 두 군데에는 반환 매개변수가 강점을 갖고 있습니다.
첫 번째는, 매개변수가 입력 매개변수( in-parameter )도 되고, 반환 매개변수( out-parameter )가 되기도 하는 경우입니다.
다음 예문은 벡터의 합을 구하고 그 값이 100 이하면, 새로운 원소를 추가하는 함수를 보여줍니다.
#include <vector>
#include <numeric> // for accumulate
using namespace std;
// in-out parameter 사용
int addMember_getSum( vector<int>& vec, int newMember){
int sum = std::accumulate( vec.begin(), vec.end(), 0);
if ( sum <= 100){
vec.push_back(newMember);
sum += newMember;
}
return sum;
}
이 경우는 매개변수 vec 를 이미 전달받았고, 수정도 가능하므로( out-parameter ), 이 컨테이너의 총합이 100 이 안 넘는 것을 확인하면 간단히 매개변수를 수정함으로써 목적을 달성할 수 있습니다.
게다가, 함수의 반환 값을 다른 목적( 총합을 반환 )에도 사용할 수 있는 장점이 있습니다.
두 번째는, 거대한 자원을 움직이는 객체를 전달할 때입니다.
보통 이러한 객체를 받은 함수들이 하는 일은 객체 전체를 새로이 생성하는 것보다, 기존 객체의 일부분을 수정하는 것입니다.
그런데, 이러한 객체를 수정하기 위해 다음과 같은 코드를 작성할 수는 없습니다.
ResourceObj modifyObject( const ResourceObj& obj){
ResourceObj newObj = obj;
// modify newObj
// ...
return newObj;
}
int main(){
ResourceObj obj;
// do something...
obj = modifyObject( obj );
}
정리
반환 매개변수( out-parameter )는 생각보다 함수의 기능을 제대로 설명하지 못합니다.
그러므로, 사용하기 전에 다른 방법이 더 나은지 따져보아야 합니다.
이 글과 관련된 글들
함수에서 std::vector와 같은 객체를 반환하는 방법
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 범위 기반 for를 사용하기 위한 사용자 타입의 필요조건 (0) | 2024.11.09 |
---|---|
[C++] 연관된 기능을 묶기 위한 중첩 타입( nested type ) (0) | 2024.11.08 |
[C++] 함수에서 std::vector와 같은 객체를 반환하는 방법 (8) | 2024.11.03 |
[C++] 한정 이름( qualified name ) 사용을 편리하게 하는 using 선언문과 using 지시문 (1) | 2024.10.31 |
[C++] 템플릿의 부분 특수화( partial specialization ) (0) | 2024.10.26 |