Почему этот код специализации по явному шаблону C является незаконным?

#c #templates #language-specifications #explicit-specialization

#c #шаблоны #язык-спецификации #явная специализация

Вопрос:

(Примечание: я знаю, насколько это незаконно, я ищу причину, по которой язык делает это таким.)

 template<class c> void Foo();  // Note: no generic version, here or anywhere.

int main(){
  Foo<int>();
  return 0;
}

template<> void Foo<int>();
  

Ошибка:

 error: explicit specialization of 'Foo<int>' after instantiation
  

Быстрый поиск в Google обнаружил эту ссылку на спецификацию, но она предлагает только «что», а не «почему».

Редактировать:

Несколько ответов переслали аргумент (например, подтвердили мое предположение) о том, что правило таково, потому что поступить иначе нарушило бы правило единого определения (ODR). Однако это очень слабый аргумент, потому что в данном случае он не выполняется по двум причинам:

  1. Перемещение явной специализации в другую единицу перевода решает проблему и, похоже, не нарушает ODR (по крайней мере, так говорит компоновщик).
  2. Краткая форма ODR (применительно к функциям) заключается в том, что у вас не может быть более одного тела для любой заданной функции, а у меня нет. Единственное место, где когда-либо определяется тело функции, — это явная специализация, поэтому вызов Foo<int> не может определить общую специализацию шаблона, потому что нет общего тела, которое можно было бы специализировать.

Размышления по этому поводу:

Предположение о том, почему правило вообще существует: если бы в первой строке предлагалось определение (в отличие от объявления), явная специализация после создания экземпляра была бы проблемой, потому что вы получили бы несколько определений. Но в этом случае единственное определение в поле зрения — это явная специализация.

Как ни странно, следующее (или что-то подобное в реальном коде, над которым я работаю) работает:

Файл A:

 template<class c> void Foo();

int main(){
  Foo<int>();
  return 0;
}
  

Файл B:

 template<class c> void Foo();

template<> void Foo<int>();
  

Но использование этого в целом приводит к созданию структуры импорта спагетти.

Комментарии:

1. «Как ни странно, работает следующее» … неверно сформирован; диагностика не требуется.

Ответ №1:

Предположение о том, почему правило вообще существует: если бы в первой строке предлагалось определение (в отличие от объявления), явная специализация после создания экземпляра была бы проблемой, потому что вы получили бы несколько определений. Но в этом случае единственное определение в поле зрения — это явная специализация.

Но у вас есть несколько определений. Вы уже определили Foo< int> при создании его экземпляра, и после этого вы пытаетесь специализировать шаблонную функцию для int, которая уже определена.

 int main(){
  Foo<int>();    // Define Foo<int>();
  return 0;
}

template<> void Foo<int>(); // Trying to specialize already defined Foo<int>
  

Комментарии:

1. Хотя это может быть технически правильно, это не отвечает на мой вопрос о том, почему. — Как можно Foo<inT>() что-либо определить? Компилятор понятия не имеет, каким будет тело функции, поэтому лучшее, что он может сделать, это выяснить, каково (искаженное) имя функции, и ввести вызов, который компоновщик исправит. Более того, если это действительно определяло Foo<in> здесь, то даже перемещение явной специализации в другой файл не должно устранять проблему, поскольку у вас все равно было бы нарушение правила единого определения.

2. Какого размера должен быть объект для выделения? Явные специализации могут полностью изменить объект, поэтому он может быть больше или меньше. Ваш main () никоим образом не будет скорректирован, чтобы компенсировать это.

Ответ №2:

Этот код является незаконным, потому что явная специализация появляется после создания экземпляра. По сути, компилятор сначала увидел универсальный шаблон, затем он увидел его создание и специализировал этот универсальный шаблон с определенным типом. После этого он увидел конкретную реализацию универсального шаблона, который уже был создан. Итак, что должен делать компилятор? Вернитесь назад и скомпилируйте код заново? Вот почему это не разрешено делать.

Комментарии:

1. Как компилятор мог специализировать этот общий шаблон с определенным типом? Нет тела функции для работы. Единственное, что у него есть на данный момент, — это прототип. (см. Комментарий Магнусу Скогу)

Ответ №3:

Вы должны думать о явной специализации как о объявлении функции. Точно так же, как если бы у вас были две перегруженные функции (без шаблонов), если до того, как вы попытаетесь выполнить вызов второй версии, можно найти только одно объявление, компилятор скажет, что он не может найти требуемую перегруженную версию. Разница с шаблонами заключается в том, что компилятор может генерировать эту специализацию на основе общего шаблона функции. Итак, почему это запрещено делать? Потому что полная специализация шаблона нарушает ODR, когда это видно, поскольку к тому времени уже существует специализация шаблона для того же типа. При создании экземпляра шаблона (неявно или нет) также создается соответствующая специализация шаблона, так что последующее использование (в той же единице перевода) той же специализации сможет повторно использовать создание экземпляра и не дублировать код шаблона для каждого создания экземпляра. Очевидно, что ODR применяется к специализациям шаблонов так же хорошо, как и в других местах.

Итак, когда в приведенном тексте говорится «диагностика не требуется», это просто означает, что компилятору не требуется предоставлять вам проницательное замечание о том, что проблема связана с созданием экземпляра шаблона, происходящим за некоторое время до явной специализации. Но, если он этого не делает, другой вариант — выдать стандартную ошибку нарушения ODR, т. Е. «множественные определения специализации ‘Foo’ для [T = int]» или что-то в этом роде, что было бы не так полезно, как более умная диагностика.

ОТВЕТ НА РЕДАКТИРОВАНИЕ

1) Хотя говорится, что все определения шаблонных функций (т. Е. Реализация) должны быть видны в момент создания экземпляра (чтобы компилятор мог подставить аргумент (ы) шаблона и создать экземпляр шаблона функции). Однако для неявного создания экземпляра шаблона функции требуется только, чтобы было доступно объявление функции. Итак, в вашем случае разделение его на две единицы перевода работает, потому что это не нарушает ODR (поскольку в этом TU есть только одно объявление Foo<int> ), объявление if Foo<int> доступно в неявной точке создания экземпляра (через Foo<T> ), а определение Foo<int> доступно компоновщику в TU B. Итак, никто не утверждал, что этот второй пример «не должен работать», он работает так, как должен. Ваш вопрос касается правила, которое применяется в пределах одной единицы перевода, не опровергайте аргументы, говоря, что ошибка не возникает, когда вы разделяете его на два TU (особенно когда это явно должно нормально работать в двух TU, согласно правилам).

2) В вашем первом примере либо будет ошибка, потому что компилятор не может найти шаблон общей функции (неспециализированная реализация) и, следовательно, не может создать экземпляр Foo<int> из общего шаблона. Или компилятор найдет определение для общего шаблона, использует его для создания экземпляра Foo<int> , а затем выдаст ошибку, поскольку встречается вторая специализация шаблона Foo<int> . Вы, кажется, думаете, что компилятор обнаружит вашу специализацию до того, как доберется до нее, это не так. Компиляторы C компилируют код сверху вниз, они не переходят туда и обратно, чтобы заменять материал здесь и там. Когда компилятор переходит к первому использованию Foo<int> , он видит только общий шаблон в этот момент, предполагает, что будет реализация этого общего шаблона, который можно использовать для создания экземпляра Foo<int> , он не ожидает специализированной реализации для Foo<int> , он ожидает и будет использовать общую. Затем он видит специализацию и выдает ошибку, потому что он уже решил, что должна была использоваться общая версия, поэтому он видит два разных определения для одной и той же функции, и да, это действительно нарушает ODR. Все очень просто.

ПОЧЕМУ, О, ПОЧЕМУ!!!

Вариант 2 TU должен сработать, потому что вы должны иметь возможность совместно использовать экземпляры шаблона между TU, это особенность C , и полезная (в случае, если у вас небольшое количество возможных экземпляров, вы можете предварительно скомпилировать их).

Случай 1 TU не может быть разрешен, потому что объявление чего-либо в C сообщает компилятору «где-то эта вещь определена». Вы сообщаете компилятору «где-то есть общее определение шаблона», затем говорите «Я хочу использовать общее определение для создания функции Foo<int> «, и, наконец, вы говорите «всякий раз, когда Foo<int> вызывается, он должен использовать это специальное определение». Это явное противоречие! Вот почему ODR существует и применяется к этому контексту, чтобы запретить такие противоречия. Не имеет значения, отсутствует ли общее определение «подлежащее поиску», компилятор ожидает его, и он должен предположить, что оно существует и что оно отличается от специализации. Он не может продолжить и сказать: «хорошо, итак, я поищу общее определение везде в коде, и если я не смогу его найти, тогда я вернусь и «одобрю» эту специализацию для использования вместо общего определения, но если я найду это, я помечу эту специализацию как ошибку». Также нельзя продолжать и категорически игнорировать желание программиста путем изменения кода, который ясно показывает намерение использовать общий шаблон (поскольку специализация еще не объявлена), для кода, который использует специализацию, которая появится позже. Я не могу объяснить «почему» более четко, чем это.

Случай 2 TU совершенно другой. Когда компилятор компилирует TU A (который использует Foo<int> ), он будет искать общее определение, не сможет его найти, предположит, что оно будет связано позже как Foo<int> , и оставляет символ-заполнитель. Затем, поскольку компоновщик не будет искать шаблоны (на практике шаблоны НЕ экспортируются), он будет искать функцию, которая реализует Foo<int> , и ему все равно, является ли это специализированной версией или нет. Компоновщик доволен, пока он находит тот же символ для ссылки. Это так, потому что делать это иначе было бы кошмаром (посмотрите обсуждения «экспортированных шаблонов») как для программистов (не имеющих возможности легко изменять функции в своих скомпилированных библиотеках), так и для поставщиков компиляторов (вынужденных реализовывать эту безумную схему компоновки).

Комментарии:

1. Я знаю, что код компилируется сверху вниз, что на самом деле является сутью проблемы: поскольку оба случая идентичны до конца main , компилятор сделает точно то же самое в обоих случаях: если он решит, что Foo<int>() использовать общий шаблон в случае 1 TU, то он примет то же решение в случае 2 TU. Почему перенос явной специализации в другой TU должен иметь какое-либо значение? (На самом деле у меня есть основания полагать, что это не так, и оба случая незаконны.) — Но все это не относится к делу, мой вопрос в том, почему (не what-of) случай 1 TU является незаконным?

2. Моя последняя правка — «почему», в этом нет сомнений. Если вас не устраивает это объяснение, я не знаю, что будет. Вы должны понимать, что за многими правилами C стояли тщательные обсуждения, исследования, и многие из них были разработаны с очень прагматичной точки зрения. Всегда существует разница между «достаточно ли это полезно» и «допускает ли это слишком много проблем или опасности для надежности языка». Я думаю, что ваш пример плохо подходит для первого и сильно влияет на второй.

3. [О том, является ли случай 2TU законным] Если вариант 1TU сообщает компилятору «где-то есть общее определение шаблона», а затем он принимает общее определение для вызова, то и вариант 2TU делает то же самое, потому что код, вплоть до конца main (после вызова), бит в бит идентичен в обоих случаях. Отсюда, чтобы случай 1TU был незаконным, он должен запретить вызов функции для (повторного?) привязки к заданному символу, который находится в том же TU, а чтобы случай 2TU был законным, он должен разрешить вызов функции для (повторного?) привязки к заданному символу, который находится в другом TU. Это звучит неправильно.

4. Кстати: на данный момент на планете есть что-то вроде дюжины людей, слову которых я бы доверил в этом вопросе.