Как компоновщик разрешает несколько определений шаблона функции в разных объектных файлах, но допускает только одно определение обычных функций

Я знаю, как использовать встроенное ключевое слово, чтобы избежать «множественного определения» при использовании шаблона С++. Однако мне любопытно, как компоновщик различает, какая специализация является полной специализацией и нарушает ODR и сообщает об ошибке, в то время как другая специализация является неявной и правильно обрабатывает ее?

Из вывода nm мы видим дублированные определения в main.o и other.o как для int-версии max(), так и для char-версии max(), но компоновщик C++ сообщает только об «множественной ошибке определения для char-версии max()». ', но позволить 'char-version max()' пройти успешную ссылку? Как компоновщик различает их и делает это?

// tmplhdr.hpp
#include <iostream>

// this function is instantiated in main.o and other.o
// but leads no 'multiple definition' error by linker
template<typename T>
T max(T a, T b)
{
    std::cout << "match generic\n";
    return (b<a)?a:b;
}

// 'multiple definition' link error if without inline
template<>
inline char max(char a, char b)
{
    std::cout << "match full specialization\n";
    return (b<a)?a:b;
}
// main.cpp
#include "tmplhdr.hpp"

extern int mymax(int, int);

int main()
{
    std::cout << max(1,2) << std::endl;
    std::cout << mymax(10,20) << std::endl;
    std::cout << max('a','b') << std::endl;
    return 0;
}
// other.cpp
#include "tmplhdr.hpp"

int mymax(int a, int b)
{
    return max(a, b);
}

Результаты теста на Ubuntu разумны; но вывод на Cygwin довольно странный и запутанный...

==== Тест на Cygwin ====

Компоновщик g++ только сообщил, что «char max (char, char)» дублируется.

$ g++ -o main.exe main.cpp other.cpp
/usr/lib/gcc/x86_64-pc-cygwin/11/../../../../x86_64-pc-cygwin/bin/ld: 
/tmp/ccYivs3O.o:other.cpp:(.text$_Z3maxIcET_S0_S0_[_Z3maxIcET_S0_S0_]+0x0): 
multiple definition of `char max<char>(char, char)'; 
/tmp/cc7HJqbS.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

Я сбросил свой объектный файл .o и не нашел много подсказок (возможно, я не совсем знаком со спецификацией формата объекта).

$ nm main.o | grep max | c++filt.exe
0000000000000000 p .pdata$_Z3maxIcET_S0_S0_
0000000000000000 p .pdata$_Z3maxIiET_S0_S0_
0000000000000000 t .text$_Z3maxIcET_S0_S0_
0000000000000000 t .text$_Z3maxIiET_S0_S0_
0000000000000000 r .xdata$_Z3maxIcET_S0_S0_
0000000000000000 r .xdata$_Z3maxIiET_S0_S0_
0000000000000000 T char max<char>(char, char) <-- full specialization
0000000000000000 T int max<int>(int, int) <<-- implicit specialization
                 U mymax(int, int)
$ nm other.o | grep max | c++filt.exe
0000000000000000 p .pdata$_Z3maxIcET_S0_S0_
0000000000000000 p .pdata$_Z3maxIiET_S0_S0_
0000000000000000 t .text$_Z3maxIcET_S0_S0_
0000000000000000 t .text$_Z3maxIiET_S0_S0_
0000000000000000 r .xdata$_Z3maxIcET_S0_S0_
0000000000000000 r .xdata$_Z3maxIiET_S0_S0_
000000000000009b t _GLOBAL__sub_I__Z5mymaxii
0000000000000000 T char max<char>(char, char) <-- full specialization
0000000000000000 T int max<int>(int, int) <-- implicit specialization
0000000000000000 T mymax(int, int)

==== Тест на Ubuntu ====

Это то, что я получил на своем Ubuntu с g++-9 после удаления inline из tmplhdr.hpp

tony@Win10Bedroom:/mnt/c/Users/Tony Su/My Documents/cpphome$ g++ -o main main.o other.o
/usr/bin/ld: other.o: in function `char max<char>(char, char)':
other.cpp:(.text+0x0): multiple definition of `char max<char>(char, char)'; main.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

'char-version max()' помечен T, что не позволяет иметь несколько определений; но «in-версия max()» помечена как W, что допускает несколько определений. Тем не менее, мне начинает любопытно, почему nm дает разные оценки на Cygwin, чем на Ubuntu ?? и Почему компоновщик на Cgywin может правильно обрабатывать два определения T?

tony@Win10Bedroom:/mnt/c/Users/Tony Su/My Documents/cpphome$ nm main.o | grep max | c++filt
0000000000000133 t _GLOBAL__sub_I__Z3maxIcET_S0_S0_
0000000000000000 T char max<char>(char, char)
0000000000000000 W int max<int>(int, int)
                 U mymax(int, int)
tony@Win10Bedroom:/mnt/c/Users/Tony Su/My Documents/cpphome$ nm other.o | grep max | c++filt
00000000000000d7 t _GLOBAL__sub_I__Z3maxIcET_S0_S0_
0000000000000000 T char max<char>(char, char)
0000000000000000 W int max<int>(int, int)
000000000000003e T mymax(int, int)

Полная специализация больше не является шаблоном (нечего выводить) и ведет себя как «обычная» функция (которая также требует inline в заголовке).

BoP 20.03.2022 09:08

Ваш c++filt меняет вывод? В Linux nm показывает W (для символов слабый) вместо T (для символов текст) для этих экземпляров. Кроме того, ваш код компилируется в Linux с помощью GCC 7.5. Так что, возможно, предоставление вашей версии компилятора также может помочь (и важно) воспроизвести проблему.

J.P.S. 20.03.2022 10:26

@Дж.П.С. мой g++ - 11.2, а c++filt не меняет второй столбец, но я нахожусь в среде Cygwin. Если в Linux есть W, то я могу понять, почему на этапе связывания нет нарушения ODR. Мой код, который я представил, может компилироваться, но если я удалю ключевое слово «inline» из tmplhdr.hpp, компиляция завершится ошибкой. извините за непоследовательность в моем вопросе.

Tony Su 20.03.2022 12:58

А, думаю, я вижу проблему. Попробуйте сначала скомпилировать оба файла по отдельности с помощью g++ -c main.cpp и g++ -c other.cpp. Затем проверьте символы с nm в обоих объектных файлах (main.o и other.o). Вы можете заметить разницу с ключевым словом inline и без него. В моем случае оба символа max были слабыми в обоих объектных файлах, если я не удалил ключевое слово inline, и в этом случае одним из четырех символов был T, что могло вызвать конфликт во время компоновки (между символом T и W символом max(char,char) обоих объектные файлы).

J.P.S. 20.03.2022 19:24

@Дж.П.С. Да, это ответ, который я ищу. Мы, наконец, получили это. Спасибо.

Tony Su 21.03.2022 01:37

@TonySu Если вам интересно, куда делись мои комментарии: похоже, мои комментарии больше не служат цели (вопрос был улучшен), поэтому я удалил их. Не стесняйтесь удалять свои собственные комментарии (особенно ответы мне), если вы считаете, что они изжили свою полезность, и вы хотите сохранить этот раздел комментариев в чистоте. ;) (Не удаляйте их, если вы видите ценность в их сохранении.)

JaMiT 21.03.2022 05:47

@JaMiT Я все еще очень ценю ваши подсказки, которые, наконец, помогли мне прояснить мои вопросы и предоставить минимальный экспериментальный пример кода.

Tony Su 21.03.2022 12:12
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
77
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

However, I start to be curious why nm gives different marks on Cygwin than on Ubuntu?? and Why linker on Cgywin can handle two T definitions correctly?

Вы должны понимать, что вывод nmнет дает вам полную картину.

nm является частью binutils и использует libbfd. Это работает следующим образом: различные форматы объектных файлов анализируются в libbfd-внутреннее представление, а затем такие инструменты, как nm, печатают это внутреннее представление в удобочитаемом формате.

Некоторые вещи "теряются при переводе". Вот почему вы должны ~ никогда не использовать, например. objdump просматривать ELF файлы (по крайней мере, не таблицу символов ELF файлов).

Как вы правильно поняли, причина, по которой в Linux разрешено использование нескольких символов max<int>(), заключается в том, что компилятор выдает их как символ W (слабо определенный).

То же самое верно и для Windows, кроме Windows использует более старый формат COFF, в котором нет слабых символов. Вместо этого символ помещается в специальный раздел .linkonce.$name, и компоновщик знает, что он может выбрать любой такой раздел в ссылку, но должен делать только это однажды (т. е. он знает, что нужно отбросить все другие дубликаты этого раздела в любом другом объектном файле). ).

Ага. вы разгадали загадку среды Cygwin. Теперь я думаю, что загадка решена как в среде Linux, так и в среде Cygwin. Очень ценю ваш ответ!!!

Tony Su 21.03.2022 01:39

Другие вопросы по теме