C++ - 자유 함수(free function) 및 주소 지정 가능한 함수(addressable function)
C++에서 free function은 클래스의 멤버를 제외한 함수를 의미합니다. 관련해서 마이크로소프트의 문서를 보면,
Functions (C++)
; https://learn.microsoft.com/en-us/cpp/cpp/functions-cpp
이런 설명이 나오는데요,
Functions that are defined at class scope are called member functions. In C++, unlike other languages, a function can also be defined at namespace scope (including the implicit global namespace). Such functions are called free functions or non-member functions; they're used extensively in the Standard Library.
쉽게 말하면, 클래스 바깥에서 정의된 전역 함수와 정적 함수를 의미합니다.
그다음 "addressable function"에 대한 설명을 다음의 문서에서 찾을 수 있습니다.
Addressing restriction
; https://en.cppreference.com/w/cpp/language/extending_std#Addressing_restriction
The behavior of a C++ program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a pointer, reference (for free functions and static member functions) or pointer-to-member (for non-static member functions) to a standard library function or an instantiation of a standard library function template, unless it is designated an addressable function (see below).
간단하게 말하면, "addressable function"이라고 지정한 함수만이 포인터 변수로 받을 수 있다는 것을 의미하는데요, 현재 C++ 표준 라이브러리의 경우 (위의 문서에 따라)
"Designated addressable functions"에 명시한 "I/O manipulators" 관련 함수들만이 "addressable function"이라고 합니다.
뭐랄까, 이건 정책의 문제로 해석하는 편이 좋을 듯합니다. 예를 들어, 제가 mycvt라는 함수를 정의했고 그것을 "addressable function"이라고 문서에 명시했다면 다음과 같은 코드가 안전할 수 있습니다.
#include <algorithm>
#include <string>
int mycvt(int c)
{
return c;
}
int main()
{
std::wstring name;
std::transform(name.begin(), name.end(), name.begin(), mycvt);
}
하지만, mycvt가 "addressable function"이 아니라고 명시했다면, 그것은 향후에 다른 오버로드를 정의할 수 있다는 입장을 취하는 것과 같습니다. 실제로 char 버전의 mycvt를 추가하면,
int mycvt(int c)
{
return c;
}
char mycvt(char c)
{
return c;
}
int main()
{
std::wstring name;
std::transform(name.begin(), name.end(), name.begin(), mycvt); // 컴파일 오류
}
이제 transform 코드는 "error C2672: 'std::transform': no matching overloaded function found" 컴파일 오류가 발생합니다. 이런 문제를 피하기 위해 "addressable function"이 아닌 함수를 저런 상황에서 써야 한다면 람다 표현을 사용할 수 있습니다.
std::transform(name.begin(), name.end(), name.begin(), [](auto c) { return mycvt(c); });
위의 코드라면, name 변수가 wstring 타입이라면 "int mycvt(int c)" 함수를 호출하고, string 타입이라면 "char mycvt(char c)" 함수를 호출합니다.
개인적으로, 위의 예제 정도는 이해가 되는데요, 반면 문서에 나온 예제는 이해가 잘 안 됩니다.
// 원본 예제에서는 std::betaf, std::riemann_zetaf를 사용했지만, 여기서는 std::tolower를 사용했습니다.
// https://en.cppreference.com/w/cpp/language/extending_std#Addressing_restriction
// Following code was well-defined in C++17, but leads to unspecified behaviors and possibly fails to compile since C++20:
#include <iostream>
int main()
{
// by unary operator&
auto fptr0 = &static_cast<int(&)(int)>(std::tolower);
std::wcout << (wchar_t)fptr0('C') << "\n";
// by std::addressof
auto fptr1 = std::addressof(static_cast<int(&)(int)>(std::tolower));
std::wcout << (wchar_t)fptr1('C') << "\n";
// by function-to-pointer implicit conversion
auto fptr2 = static_cast<int(&)(int)>(std::tolower);
std::wcout << (wchar_t)fptr2('C') << "\n";
// forming a reference
auto& fref = static_cast<int(&)(int)>(std::tolower);
std::wcout << (wchar_t)fref('C') << "\n";
}
문서에서는 위의 코드가 C++ 17에서는 컴파일이 되지만, C++ 20에선 컴파일이 되지 않을 수 있다고 합니다. 아마도 그것은 저 문서가 C++ 17 당시에 작성됐을 것이므로 향후 버전에서의 변화를 알 수 없어 그렇게 적혀 있는 것이 맞겠습니다.
하지만, 저 코드를 보면 앞서 예제를 들었던 transform + mycvt와는 다르게 static_cast 시 함수 시그니처를 함께 지정했으므로 문제될 것이 없습니다. 즉, mycvt 함수를 transform에 다음과 같이 넘긴 경우로 보면 되는데요,
{
std::wstring name = L"TEST";
auto fptr0 = &static_cast<int(&)(int)>(mycvt); // int mycvt(int c) 버전 선택
std::transform(name.begin(), name.end(), name.begin(), fptr0);
}
당연히 위의 코드는 "char mycvt(char c)" 버전이 추가된다고 해도 컴파일이 잘 됩니다. 혹시, C++에 대해 잘 아시는 분이 계시다면, 왜 저 코드가 "was well-defined in C++17, but leads to unspecified behaviors and possibly fails to compile since C++20"라고 적혀 있는지 설명을 좀 덧글로 부탁드립니다. ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]