Является ли возвращаемый тип функции частью искаженного имени?

Предположим, у меня есть две функции с одинаковыми типами параметров и именем (не в одной программе):

std::string foo(int x) {
  return "hello"; 
}

int foo(int x) {
  return x;
}

Будут ли они иметь одно и то же искаженное имя после компиляции?

Является ли возвращаемый тип частью искаженного имени в С++?


person sdgfsdh    schedule 24.11.2016    source источник
comment
Может быть, а может и нет. Это зависит от компилятора.   -  person Pete Becker    schedule 24.11.2016
comment
Дарвин разбирает это. Компиляторы, которые позволяют связать совершенно неправильную функцию или производят (почти) недиагностируемую ошибку компоновщика, не используются.   -  person Hans Passant    schedule 24.11.2016


Ответы (2)


Поскольку схемы искажения не стандартизированы, на этот вопрос нет однозначного ответа; ближе всего к реальному ответу было бы посмотреть на искаженные имена, сгенерированные наиболее распространенными схемами искажения. Насколько мне известно, это схемы GCC и MSVC в алфавитном порядке, так что...


ССЗ:

Чтобы проверить это, мы можем использовать простую программу.

#include <string>
#include <cstdlib>

std::string foo(int x) { return "hello"; }
//int         foo(int x) { return x; }

int main() {
    // Assuming executable file named "a.out".
    system("nm a.out");
}

Скомпилируйте и запустите с помощью GCC или Clang, и он выведет список содержащихся в нем символов. В зависимости от того, какая из функций раскомментирована, результаты будут такими:

// GCC:
// ----

std::string foo(int x) { return "hello"; } // _Z3fooB5cxx11i
                                             // foo[abi:cxx11](int)
int         foo(int x) { return x; }       // _Z3fooi
                                             // foo(int)

// Clang:
// ------

std::string foo(int x) { return "hello"; } // _Z3fooi
                                             // foo(int)
int         foo(int x) { return x; }       // _Z3fooi
                                             // foo(int)

Схема GCC содержит относительно мало информации, не считая возвращаемых типов:

  • Тип символа: _Z для функции.
  • Имя: 3foo для ::foo.
  • Параметры: i для int.

Однако, несмотря на это, они различаются при компиляции с помощью GCC (но не с Clang), потому что GCC указывает, что версия std::string использует cxx11 ABI.

Обратите внимание, что он по-прежнему отслеживает тип возвращаемого значения и следит за совпадением подписей; просто для этого не используется искаженное имя функции.


МСВК:

Чтобы проверить это, мы можем использовать простую программу, как указано выше.

#include <string>
#include <cstdlib>
    
std::string foo(int x) { return "hello"; }
//int         foo(int x) { return x; }
    
int main() {
    // Assuming object file named "a.obj".
    // Pipe to file, because there are a lot of symbols when <string> is included.
    system("dumpbin/symbols a.obj > a.txt");
}

Скомпилируйте и запустите с помощью Visual Studio, и a.txt выведет список содержащихся в нем символов. В зависимости от того, какая из функций раскомментирована, результаты будут такими:

std::string foo(int x) { return "hello"; }
  // ?foo@@YA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@H@Z
  // class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > __cdecl foo(int)
int         foo(int x) { return x; }
  // ?foo@@YAHH@Z
  // int __cdecl foo(int)

Схема MSVC содержит все объявление, включая то, что не указано явно:

  • Имя: foo@ для ::foo, затем @ для завершения.
  • Тип символа: Все после @, завершающего имя.
  • Тип и статус члена: Y для функции, не являющейся членом.
  • Соглашение о вызовах: A для __cdecl.
  • Return type:
    • H for int.
    • ?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@ (за которым следует @ для завершения) для std::basic_string<char, std::char_traits<char>, std::allocator<char>> (сокращенно std::string).
  • Список параметров: H для int (за которым следует @ для завершения).
  • Спецификатор исключения: Z для throw(...); это опущено из разделенных имен, если это не что-то другое, вероятно, потому, что MSVC все равно просто игнорирует его.

Это позволяет ему ныть, если объявления не идентичны в каждой единице компиляции.


Как правило, большинство компиляторов будут использовать одну из этих схем (или иногда их вариант) при работе с *nix или Windows соответственно, но это не гарантируется. Например...

  • Clang, насколько мне известно, будет использовать схему GCC для *nix или схему MSVC для Windows.
  • Intel C++ использует схему GCC для Linux и Mac и схему MSVC (с некоторыми незначительными вариациями) для Windows.
  • У компиляторов Borland и Watcom свои схемы.
  • Компиляторы Symantec и Digital Mars обычно используют схему MSVC с небольшими изменениями.
  • Старые версии GCC и многие инструменты UNIX используют модифицированную версию схемы искажения cfront.
  • И так далее...

Схемы, используемые другими компиляторами, взяты из PDF Agner Fog.


Примечание:

Изучив сгенерированные символы, становится очевидным, что схема искажения GCC не обеспечивает такого же уровня защиты от Макиавелли, как MSVC. Рассмотрим следующее:

// foo.cpp
#include <string>

// Simple wrapper class, to avoid encoding `cxx11 ABI` into the GCC name.
class MyString {
    std::string data;

  public:
    MyString(const char* const d) : data(d) {}

    operator std::string() { return data; }
};

// Evil.
MyString foo(int i) { return "hello"; }

// -----

// main.cpp
#include <iostream>

// Evil.
int foo(int);

int main() {
    std::cout << foo(3) << '\n';
}

Если мы скомпилируем каждый исходный файл отдельно, то попытаемся связать объектные файлы вместе...

  • GCC: MyString из-за того, что он не является частью cxx11 ABI, приводит к тому, что MyString foo(int) искажается как _Z3fooi, как и int foo(int). Это позволяет связать объектные файлы и создать исполняемый файл. Попытка запустить его вызывает segfault.
  • MSVC: компоновщик будет искать ?foo@@YAHH@Z; поскольку вместо этого мы указали ?foo@@YA?AVMyString@@H@Z, связывание не удастся.

Учитывая это, схема изменения, включающая тип возвращаемого значения, более безопасна, хотя функции нельзя перегружать исключительно из-за различий в типе возвращаемого значения.

person Justin Time - Reinstate Monica    schedule 24.11.2016

Нет, и я ожидаю, что их искаженное имя будет одинаковым для всех современных компиляторов. Что еще более важно, их использование в одной и той же программе приводит к неопределенному поведению. Функции в C++ не могут отличаться только типом возвращаемого значения.

person Sam Varshavchik    schedule 24.11.2016
comment
Фактически неверно по 2 пунктам. 1) Искаженные имена Visual Studio содержат полный тип сущности; это функции ?foo@@YA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@H@Z и ?foo@@YAHH@Z соответственно. Точно так же искаженные имена GCC также содержат некоторую информацию о типе, хотя и не так много, как имена MSVC; это функции _Z3fooB5cxx11i и _Z3fooi соответственно (тип возвращаемого значения не сохраняется, но функция std::string определяет, что она использует cxx11 ABI). (В некоторых версиях GCC они могут быть идентичными.) - person Justin Time - Reinstate Monica; 24.11.2016
comment
2) Нет оснований предполагать, что все компиляторы используют одну и ту же схему искажения; вполне возможно (и отчасти вероятно), что каждый компилятор будет использовать свой собственный. Например, Visual Studio и GCC имеют свои собственные схемы искажения, и многие другие компиляторы используют их схемы в качестве неофициальных стандартов для Windows и *nix соответственно. Компиляторы Borland и Watcom также имеют свои собственные уникальные схемы, а более старые версии GCC (наряду со многими инструментами UNIX) используют модифицированную версию схемы искажения cfront. - person Justin Time - Reinstate Monica; 24.11.2016
comment
Компилятор Intel обычно использует схему MSVC для Windows и современную схему GCC для *nix. Symantec и Digital Mars обычно используют схему MSVC с небольшими изменениями. И так далее. См. здесь, это довольно интересно. - person Justin Time - Reinstate Monica; 24.11.2016
comment
Насколько мне известно, ни одна из них не будет компилировать код как есть, но если каждая функция компилируется отдельно, их имена не обязательно будут одинаковыми. - person Justin Time - Reinstate Monica; 24.11.2016
comment
(Они будут идентичны, если скомпилированы Clang в Linux, хотя, например, даже если он использует схему GCC. Она варьируется от компилятора к компилятору.) - person Justin Time - Reinstate Monica; 24.11.2016
comment
Я согласен с вами, исходя из того, что тип возвращаемого значения должен быть уникальным. В любом случае изменение типа возвращаемого значения добавит дополнительную безопасность в том маловероятном случае, когда отдельные модули компиляции определяют такие конфликтующие функции, но они никогда не объявляются в одном и том же модуле. - person Yves Daoust; 25.11.2016