C++ 獲取指定的過載函數地址

2022-06-08 09:01:31

剛剛看到一篇部落格,說 std::bind 無法系結正確的過載函數。這裡的問題並不是 std::bind 能力不足,而是將函數名傳遞給 std::bind 時編譯器無法取到這個函數的地址(也就是符號,編譯器會先解析成符號,連結器再替換為地址),因為有多個過載函數都是這個名字。核心問題是無法通過函數名取到想要的過載函數地址。就像下面的程式碼無法編譯通過:

#include <iostream>

void f()
{
    std::cout << "f 1" << std::endl;
}

void f(int x)
{
    std::cout << "f 2 " << x << std::endl;
}

int main()
{
    auto p = &f;
}

編譯錯誤:

/home/abc/cpplearn/overload_func.cpp: In function ‘int main()’:
/home/abc/cpplearn/overload_func.cpp:15:15: error: unable to deduce ‘auto’ from ‘& f’
   15 |     auto p = &f;
      |               ^
/home/abc/cpplearn/overload_func.cpp:15:15: note:   couldn’t deduce template parameter ‘auto’

有沒有什麼比較完美的解決辦法呢?我覺得一定有,因為 C 語言沒有函數過載,函數地址作為實參也是常規操作。相比之下,C++ 引入了函數過載,卻無法取到函數地址,這就很尷尬。C++ 設計者肯定也想到了這個問題。

於是查閱了 cppreference.com,看到了 Address of an overloaded function。函數名的過載解析除了發生在函數呼叫的時候,也會發生在以下 7 種語境:

# Context Target
1 initializer in a declaration of an object or reference the object or reference being initialized
2 on the right-hand-side of an assignment expression the left-hand side of the assignment
3 as a function call argument the function parameter
4 as a user-defined operator argument the operator parameter
5 the return statement the return type of a function
6 explicit cast or static_cast argument the target type of a cast
7 non-type template argument the type of the template parameter

當函數名存在於這 7 種語境時,會發生過載解析,並且會選擇與 Target 型別匹配的那個過載函數。這裡就不一一考察這 7 種語境了,有興趣可以自己查閱 cppreference.com。這裡重點考察第 3 種和第 6 種。

先看第 3 種語境。當函數名作為函數呼叫的實參時,過載解析會選擇和形參型別相匹配的版本。也就是說,下面的程式碼會如期執行:

#include <iostream>

void f()
{
    std::cout << "f 1" << std::endl;
}

void f(int x)
{
    std::cout << "f 2 " << x << std::endl;
}

void call(void p(int)) {
    p(1);
}

int main()
{
    call(f);
}

這段程式碼輸出:

f 2 1

回到最初的問題,std::bind 也是函數,為什麼無法正常編譯呢?直接分析下面程式碼的編譯錯誤資訊:

#include <iostream>
#include <functional>

void f()
{
    std::cout << "f 1" << std::endl;
}

void f(int x)
{
    std::cout << "f 2 " << x << std::endl;
}

int main()
{
    auto new_func = std::bind(f, std::placeholders::_1);
    new_func(66);
}

編譯錯誤:

/home/abc/cpplearn/overload_func.cpp: In function ‘int main()’:
/home/abc/cpplearn/overload_func.cpp:16:30: error: no matching function for call to ‘bind(<unresolved overloaded function type>, const std::_Placeholder<1>&)’
   16 |     auto new_func = std::bind(f, std::placeholders::_1);
      |                     ~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~

可以看到,std::bind 準確地說是一個函數模板。它要根據其引數進行模板實參推導,再替換模板形參進行範例化(Instantiation),產生和普通函數類似的組合程式碼。std::bind 進行範例化的時候,函數 f 還沒有進行過載解析,其型別為<unresolved overloaded function type>。std::bind 無法進行範例化。怎樣修改可以解決這個問題呢?

可以利用第 6 個語境,也就是顯示轉換或 static_cast。過載解析會選擇與它們的目標型別相匹配的版本。下面的程式碼會如期執行:

#include <iostream>
#include <functional>

void f()
{
    std::cout << "f 1" << std::endl;
}

void f(int x)
{
    std::cout << "f 2 " << x << std::endl;
}

int main()
{
    auto new_func = std::bind((void(*)(int))f, std::placeholders::_1);
    new_func(66);
}

這段程式碼輸出:

f 2 66

還有一種更加巧妙的辦法,依然是利用第 3 種語境。既然 std::bind 的第一個模板實參的推導,和 f 的過載解析相矛盾。為什麼不直接解決這個矛盾,將第一個模板實參改為顯示指定?來看下面的程式碼:

#include <iostream>
#include <functional>

void f()
{
    std::cout << "f 1" << std::endl;
}

void f(int x)
{
    std::cout << "f 2 " << x << std::endl;
}

int main()
{
    auto new_func = std::bind<void(int)>(f, std::placeholders::_1);
    new_func(66);
}

這段程式碼如期輸出:

f 2 66