Частичное упорядочение с помощью шаблона функции, имеющего необузданный контекст

StackOverflow https://stackoverflow.com/questions/1180325

Вопрос

Читая другой вопрос, я столкнулся с проблемой частичного упорядочения, которую я сократил до следующего тестового примера

template<typename T>
struct Const { typedef void type; };

template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1

template<typename T>
void f(T, void*) { cout << "void*"; } // T2

int main() {
  // GCC chokes on f(0, 0) (not being able to match against T1)
  void *p = 0;
  f(0, p);
}

Для обоих шаблонов функций типом функции специализации, которая вводит разрешение перегрузки, является void(int, void*).Но частичное упорядочение (в соответствии с comeau и GCC) теперь говорит о том, что второй шаблон более специализированный.Но почему?

Позвольте мне выполнить частичный заказ и показать, где у меня есть вопросы.Май Q быть уникальным составным типом, используемым для определения частичного упорядочения в соответствии с 14.5.5.2.

  • Преобразованный список параметров для T1 (Вставлен Q): (Q, typename Const<Q>::type*).Типами аргументов являются AT = (Q, void*)
  • Преобразованный список параметров для T2 (Вставлен Q): BT = (Q, void*), которые также являются типами аргументов.
  • Нетрансформированный параметр-список для T1: (T, typename Const<T>::type*)
  • Нетрансформированный параметр-список для T2: (T, void*)

Поскольку C ++ 03 не уточняет это, я использовал намерение, о котором читал в нескольких отчетах о дефектах.Приведенный выше преобразованный список параметров для T1 (вызываемый AT мной) используется в качестве списка аргументов для 14.8.2.1 "Вывод аргументов шаблона из вызова функции".

14.8.2.1 не нуждается в преобразовании AT или BT себя больше (например, удаление деклараторов ссылок и т.д.), И переходит прямо к 14.8.2.4, которые независимо для каждого A / P пара выполняет вывод типа:

  • AT против T2: { (Q, T), (void*, void*) }. T является единственным параметром шаблона здесь, и он обнаружит, что T должно быть Q.Вывод типа выполняется тривиально для AT против T2.

  • BT против T1: { (Q, T), (void*, typename Const<T>::type*) }.Он обнаружит, что T является Q, тоже здесь. typename Const<T>::type* это неразведенный контекст, и поэтому он не будет использоваться для вывода чего-либо.


Вот мой первый вопрос:Будет ли это теперь использовать значение T выведено для первого параметра?Если ответ отрицательный, то первый шаблон более специализированный.Этого не может быть, потому что и GCC, и Comeau говорят, что второй шаблон более специализированный, и я не верю, что они ошибаются.Итак, мы предполагаем "да" и вставляем void* в T.Абзац (14.8.2.4) говорит "Вычет выполняется независимо для каждой пары, а затем результаты объединяются" а также "Однако в определенных контекстах значение не участвует в выводе типа, а вместо этого использует значения аргументов шаблона, которые были либо выведены в другом месте, либо явно указаны". Это тоже звучит как "да".

Таким образом, дедукция также выполняется успешно для каждой пары A / P.Теперь каждый шаблон, по крайней мере, столь же специализирован, как и другой, потому что дедукция также не зависела от каких-либо неявных преобразований и была успешной в обоих направлениях.В результате вызов должен быть неоднозначным.

Итак, мой второй вопрос:Теперь, почему в реализациях говорится, что второй шаблон более специализированный?Какой момент я упустил из виду?


Редактировать:Я протестировал явную специализацию и создание экземпляра, и то и другое, в последних версиях GCC (4.4) скажите мне, что ссылка на специализацию неоднозначна, в то время как более старая версия GCC (4.1) не вызывает этой ошибки двусмысленности.Это говорит о том, что последние версии GCC имеют непоследовательный частичный порядок для шаблонов функций.

template<typename T>
struct Const { typedef void type; };

template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1

template<typename T>
void f(T, void*) { cout << "void*"; } // T2

template<> void f(int, void*) { }
  // main.cpp:11: error: ambiguous template specialization 
  // 'f<>' for 'void f(int, void*)'
Это было полезно?

Решение

Вот мой подход к этому.Я согласен с Чарльз Бейли что неправильный шаг - это идти от Const<Q>::Type* Для void*

template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1

template<typename T>
void f(T, void*) { cout << "void*"; } // T2

Шаги, которые мы хотим предпринять, это:

14.5.5.2/2

Учитывая два перегруженных шаблона функций, можно определить, является ли один из них более специализированным, чем другой, путем преобразования каждого шаблона по очереди и использования вывода аргументов (14.8.2) для сравнения его с другим.

14.5.5.2/3-b1

Для каждого параметра шаблона типа синтезируйте уникальный тип и заменяйте его для каждого вхождения этого параметра в списке параметров функции или для функции преобразования шаблона в возвращаемом типе.

На мой взгляд, типы синтезируются следующим образом:

(Q, Const<Q>::Type*)    // Q1
(Q, void*)              // Q2

Я не вижу никакой формулировки, которая требует, чтобы второй синтезированный параметр T1 быть void*.Я не знаю ни одного прецедента для этого и в других контекстах.Тип Const<Q>::Type* является абсолютно допустимым типом в системе типов C ++.

Итак, теперь мы выполняем шаги дедукции:

От Q2 до T1

Мы пытаемся вывести параметры шаблона для T1, чтобы у нас было:

  • Параметр 1: T выводится, чтобы быть Q
  • Параметр 2:Необусловленный контекст

Несмотря на то, что параметр 2 не является выведенным контекстом, вывод все равно выполнен успешно, потому что у нас есть значение для T.

От Q1 до T2

Выводя параметры шаблона для T2, мы имеем:

  • Параметр 1: T выводится, чтобы быть Q
  • Параметр 2: void* не соответствует Const<Q>::Type* итак, дедукция провалилась.

ИМХО, вот тут-то стандарт нас и подводит.Параметр не зависит, поэтому не совсем ясно, что должно произойти, однако мой опыт (основанный на прищуренном чтении 14.8.2.1 / 3) заключается в том, что даже там, где тип параметра P не зависит, тогда тип аргумента A должен соответствовать ему.

Синтезированные аргументы T1 могут быть использованы для специализации T2, но не наоборот.Таким образом, T2 более специализирован, чем T1, и поэтому является лучшей функцией.


ОБНОВЛЕНИЕ 1:

Просто для того, чтобы прикрыть вопрос о Const<Q>::type быть пустотой.Рассмотрим следующий пример:

template<typename T>
struct Const;

template<typename T>
void f(T, typename Const<T>::type*) // T1
{ typedef typename T::TYPE1 TYPE; }

template<typename T>
void f(T, void*)                    // T2
{ typedef typename T::TYPE2 TYPE ; }

template<>
struct Const <int>
{
  typedef void type;
};

template<>
struct Const <long>
{
  typedef long type;
};

void bar ()
{
  void * p = 0;
  f (0, p);
}

В приведенном выше, Const<int>::type используется, когда мы выполняем обычные правила разрешения перегрузки, но не тогда, когда мы переходим к правилам частичной перегрузки.Было бы неправильно выбирать произвольную специализацию для Const<Q>::type.Возможно, это не интуитивно понятно, но компилятор вполне доволен тем, что имеет синтезированный тип формы Const<Q>::type* и использовать его во время вывода типа.


ОБНОВЛЕНИЕ 2

template <typename T, int I>
class Const
{
public:
  typedef typename Const<T, I-1>::type type;
};

template <typename T>
class Const <T, 0>
{
public:
  typedef void type;
};

template<typename T, int I>
void f(T (&)[I], typename Const<T, I>::type*)     // T1
{ typedef typename T::TYPE1 TYPE; }

template<typename T, int I>
void f(T (&)[I], void*)                           // T2
{ typedef typename T::TYPE2 TYPE ; }


void bar ()
{
  int array[10];
  void * p = 0;
  f (array, p);
}

Когда в Const шаблон создается с некоторым значением I, он рекурсивно создает сам себя до тех пор , пока I достигает 0.Это когда частичная специализация Const<T,0> выбран.Если у нас есть компилятор, который синтезирует некоторый вещественный тип для параметров функции, то какое значение компилятор выберет для индекса массива?Скажем, 10?Что ж, это было бы прекрасно для приведенного выше примера, но это не соответствовало бы частичной специализации Const<T, 10 + 1> что, по крайней мере концептуально, привело бы к бесконечному числу рекурсивных экземпляров первичного.Какое бы значение он ни выбрал, мы могли бы изменить конечное условие на это значение + 1, и тогда у нас был бы бесконечный цикл в алгоритме частичного упорядочения.

Я не вижу, как алгоритм частичного упорядочения мог бы корректно создать экземпляр Const чтобы найти что type действительно есть.

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

Редактировать:После изучения Лязгающий реализация (Дугом Грегором) их алгоритма частичного упорядочивания, я пришел к согласию с остальными постерами в том, что исходный пример не "предназначен" для того, чтобы быть двусмысленным - даже несмотря на то, что стандарт не так ясен, как мог бы быть, в отношении того, что должно происходить в таких ситуациях.Я отредактировал этот пост, чтобы изложить свои пересмотренные мысли (для моей собственной пользы и справки).В частности, алгоритм Кланга прояснил, что 'typename Const<T>::type' не преобразуется в 'void' на этапе частичного упорядочивания - и что каждая пара A / P выводится независимо друг от друга.

Первоначально я задавался вопросом, почему следующее было сочтено двусмысленным:

        template<class T> void f(T,T*);  // 1

        template<class T> void f(T, int*); // 2

        f(0, (int*)0); // ambiguous

(The above is ambiguous because one cannot deduce f1(U1,U1*) from f2(T,int*), and going the other way, one cannot deduce f2(U2,int*) from f1(T,T*). Neither is more specialized.)

но следующее не было бы двусмысленным:

        template<class T> struct X { typedef int type; };
        template<class T> void f(T, typename X<T>::type*); // 3
        template<class T> void f(T, int*); // 2

(Причина, по которой можно было бы ожидать, что это будет неоднозначно, заключается в том, что произойдет следующее:
- f3(U1,X<U1>::type*) -> f3(U1, int*) ==> f2(T,int*) (deduction ok, T=U1)
- f2(U2,int*) ==> f3(T, X<T>::type*) (deduction ok, T=U2 makes X<U2>::type* -> int*)
Если бы это было правдой, ни одно из них не было бы более специализированным, чем другое.)

После изучения алгоритма частичного упорядочения Clang становится ясно, что они рассматривают '3' выше, как если бы это было:

template<class T, class S> void f(T, S*); // 4

таким образом, вычет некоторого уникального 'U' из 'typename X::type' будет успешным -

  • f3(U1,X<U1>::type*) is treated as f3(U1, U2*) ==> f2(T,int*) (deduction not ok)
  • f2(U2,int*) ==> f3(T,S* [[X<T>::type*]]) (deduction ok, T=U2, S=int)

Таким образом, '2' явно более специализирован, чем '3'.

Преобразованный список параметров для T1 (Q вставлен):(Вопрос, имя типа Const::тип*).Типы аргументов имеют значение = (Q, void*)

Интересно, действительно ли это правильное упрощение.Когда вы синтезируете тип Q, разрешено ли вам создавать специализацию для Const для целей определения порядка спецификации шаблона?

template <>
struct Const<Q> { typedef int type; }

Это означало бы , что T2 по крайней мере, не так специализирован, как T1 потому что a void* параметр не соответствует T1второй параметр для любых заданных параметров шаблона.

Редактировать:Пожалуйста, не обращайте внимания на этот пост - После изучения алгоритма clangs для частичного упорядочения, реализованного Дугом Грегором (хотя на момент написания этой статьи он реализован лишь частично - кажется, что логика, имеющая отношение к вопросу OP, реализована достаточно адекватно) - похоже, что он обрабатывает необразованный контекст как просто еще один параметр шаблона.Это предполагает, что перегрузка с явным аргументом void * должна быть более специализированной версией, и в ней не должно быть никакой двусмысленности.Как обычно, Комо прав.Теперь что касается формулировки в стандарте , которая четко определяет это поведение - это другой вопрос ...

Поскольку этот пост также был опубликован на comp.lang.c ++.moderated и, похоже, вызывает некоторую путаницу и там - я подумал, что опубликую свой ответ и в этой группе здесь - поскольку обсуждение, очевидно, имеет отношение к заданному здесь вопросу.

On Jul 25, 1:11 pm, Bart van Ingen Schenau <b...@ingen.ddns.info> wrote:

You are going one step too fast here. How do you know (and would the compiler know) that there is no specialisation of Const<Q> such that Const<Q>::type != void?

As far as I can see, the compiler would transform the parameter-list of A to: AT=(Q, <unknown>*). To call B with these parameters requires an implicit conversion (<unknown>* to void*) and therefore A is less specialised than B.

Я считаю, что это неверно.При проверке, какая функция является более специализированной (при частичном упорядочении), компилятор преобразует список параметров в (Q, void*) - то естьфактически он создает экземпляр соответствующего шаблона (наиболее подходящий) и ищет в нем значение 'type' - в данном случае на основе основного шаблона оно будет void *.

Что касается вашего замечания относительно частичной специализации - при проверке какой шаблон более специализирован, чем другой, единственный тип, который может быть использован это уникальный сгенерированный тип - если в точке есть другие специализации о создании экземпляра объявления (когда выполняется разрешение перегрузки) они будут рассмотрены.Если вы добавите их позже, и они должны быть выбраны вы будете нарушать ODR (в соответствии с 14.7.4.1)

Частичные / явные специализации также будут рассмотрены во время формирования набора кандидатов - но на этот раз с использованием типов фактических аргументов функции.Если наилучшая совпадающая частичная специализация (из X) приводит к типу функции, который имеет лучшую неявную последовательность преобразования для некоторого параметра, тогда мы никогда не переходим к фазе частичного упорядочивания, и это будет выбрана функция "лучше" (перед переходом к частичному этапу заказа)

Вот пример с комментариями о том, что должно происходить на различных этапах:

    template<class T, bool=true> struct X;  // Primary

    template<class T> struct X<T,true> { typedef T type; };  // A
    template<> struct X<int*,true> { typedef void* type; };  // B


    template<class T> void f(T,typename X<T>::type); //1
    template<class T> void f(T*,void*); //2


    int main()
    {
      void* pv;
      int* pi;


      f(pi,pi);   
      // two candidate functions: f1<int*>(int*,void*),  f2<int>(int*,void*)
      // Note: specialization 'B' used to arrive at void* in f1
      // neither has a better ICS than the other, so lets partially order
      // transformed f1 is f1<U1>(U1,X<U1,true>::type) --> f1<U1>(U1,U1) 
      //       (template 'A' used to get the second U1)
      // obviously deduction will fail (U1,U1) -> (T*,void*)
      // and also fails the other way (U2*, void*) -> (T,X<T>::type)
      // can not partially order them - so ambiguity 




      f(pv,pv);  
      // two candidate functions: f1<void*>(void*,void*), f2<void>(void*,void*)
      // Note: specialization 'A' used to arrive at second void* in f1
      // neither has a better ICS than the other, so lets partially order
      // transformed f1 is f1<U1>(U1,X<U1>::type) --> f1<U1>(U1,U1) 
      //       (template 'A' used to get the second U1)
      // obviously deduction will fail (U1,U1) -> (T*,void*)
      // and also fails the other way (U2*, void*) -> (T,X<T>::type)
      // can not partially order them - so ambiguity again             

    }

Также стоит упомянуть, что если у основного шаблона нет определения - тогда SFINAE работает на этапе частичного упорядочивания, ни один из них не может быть выведен из другого, и в результате должна возникнуть двусмысленность.

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

On Jul 25, 1:11 pm, Bart van Ingen Schenau <b...@ingen.ddns.info> wrote:

Во-первых, быть более специализированным означает, что это меньше типы, где этот шаблон может быть выбран с помощью разрешения перегрузки.Используя это, правила для частичного упорядочения можно резюмировать следующим образом:Попробуйте найти тип для A такой, чтобы A можно было вызвать, а B - нет, или перегрузить разрешение предпочитает вызывать A.Если этот тип может быть найден, то B является более специализированным, чем A.

Здесь нет никаких возражений.Но, основываясь на правилах в том виде, в каком они существуют в настоящее время, пример OP должен быть неоднозначным.


Наконец, вот четкие, недвусмысленные ответы на два конкретных вопроса, поднятых litb:

1) Будет ли при этом теперь использоваться значение T, выведенное для первого параметра?
Да - конечно, это необходимо, это вычитание аргумента шаблона - "ссылки" должны поддерживаться.

2) Теперь, почему реализации говорят, что вторая вместо этого более специализированная?
Потому что они неправы ;)

Я надеюсь, что это положит конец проблеме - пожалуйста, дайте мне знать, если есть что-то, что все еще неясно :)

Редактировать:литб поднял хороший вопрос в своем комментарии - возможно, заявив, что основной шаблон всегда будет использоваться использовать для создания экземпляра уникальный сгенерированный тип - слишком сильное утверждение.
Существуют случаи, когда основной шаблон вызываться не будет.
К чему я клоню, так это к тому, что при частичном упорядочении используется некоторый уникальный сгенерированный тип используется для соответствия наилучшей специализации.Вы правы, это не обязательно должен быть основной шаблон.Для этого я отредактировал приведенный выше язык.Он также поднял вопрос, касающийся определения более подходящего шаблона после создания экземпляра.Это будет нарушением ODR в соответствии с разделом о точке создания экземпляра.


Стандарт гласит, что после создания пар A / P (с использованием правил преобразования, описанных в temp.func.order) они сопоставляются друг с другом с использованием вывода аргумента шаблона (temp.deduct) - и этот раздел обрабатывает случай недведенных контекстов, создавая экземпляр шаблона и его вложенный тип, запуская точки создания экземпляров.Раздел temp.point обрабатывает нарушения ODR (значение частичного упорядочения не должно меняться независимо от точек создания экземпляра внутри единицы перевода).Я все еще не уверен, откуда берется эта путаница?– Фейсал Вали 1 час назад [удалить этот комментарий]

литб:"Обратите внимание, что шаг, который помещает Q в Const::type для построения аргументов, явно не подпадает под действие правила SFINAE.Правила SFINAE работают с вычетом аргументов, поместите абзацы, которые помещают Q в список параметров функции шаблона функции, в 14.5.5.2.'

Здесь должны быть использованы правила SFINAE - как их могло не быть?Я чувствую, что это достаточно подразумеваемо - я не буду отрицать, что это могло бы быть яснее, и хотя я призываю комитет разъяснить это - я не думаю, что это нуждается в разъяснении, чтобы адекватно интерпретировать ваш пример.

Позвольте мне предложить один из способов связать их.Из (14.8.2):"Когда указан явный список аргументов шаблона, аргументы шаблона должны быть совместимы со списком параметров шаблона результатом должен быть допустимый тип функции, как описано ниже;в противном случае введите вывод сбой"

Из (14.5.5.2/3) "Используемое преобразование является:— Для каждого параметра шаблона типа синтезируйте уникальный тип и заменяйте его для каждого вхождения этот параметр в списке параметров функции или для функции преобразования шаблона в возвращаемом типе."

На мой взгляд, приведенная выше цитата подразумевает, что как только вы "создаете" уникальные сгенерированные типы для каждого параметра шаблона, объявление функции должно быть неявно создано с помощью явно предоставление уникальных типов в качестве аргументов шаблона нашему шаблону функции.Если это приводит к недопустимому типу функции, то не выполняется не только преобразование, но, что более важно, последующее удаление аргумента шаблона, необходимое для частичного упорядочивания функции.

Из (14.5.5.2/4) "Используя преобразованный список параметров функции, выполните вычитание аргументов по сравнению с другим шаблоном функции.Преобразованный шаблон , по крайней мере, такой же специализированный, как и другой если, и только если, вывод выполняется успешно, и выведенные типы параметров являются точным совпадением (поэтому вывод не зависит от неявных преобразований)."

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

литб:Я также не уверен, что происходит в этом случае: template<typename T> struct A; template<typename T> void f(T, typename A<T>::type); template<typename T> void f(T*, typename A<T>::type); конечно, вот, предназначенное, чтобы быть действительный код, но делает::типа, это не получится, потому что в контексте определения шаблона, также пока не определена" Также обратите внимание, что для экземпляров шаблонов, возникающих в результате этого, не определен POI вид подстановки при попытке определить порядок (частичный порядок не зависит ни от какого контекста.Это статическое свойство двух задействованных шаблонов функций).Я думаю, это выглядит как проблема в Стандарте, которая должна быть исправлена.

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

Теперь из стандарта ясно, что частичный порядок не зависит от типа, который используется при вызове функции (я полагаю это то, что вы имеете в виду, когда описываете это как статическое свойство, и оно не зависит от контекста).

В стандарте также ясно, что он заботится только о частичном упорядочении (вызывает частичный порядок) между шаблонами функций в процессе разрешения перегрузки (13.3.3 / 1) тогда и только тогда, когда не удалось выбрать лучшую функцию на основе ICS или если одна является шаблоном, а другая нет.[Частичное упорядочение шаблонов классов частичные специализации - это отдельная проблема и, на мой взгляд, использует соответствующий контекст (другие определения шаблонов), который требует создания экземпляра этого конкретного класса.]

Таким образом, на мой взгляд, поскольку механизм частичного упорядочения шаблонов функций вызывается при перегрузке выполняется разрешение, оно должно использовать соответствующую часть контекста (определения шаблонов и специализации), доступную в тот момент, когда выполняется разрешение перегрузки.

Итак, основываясь на моем понимании, согласно вашему примеру с использованием 'template struct A' выше, код является допустимым.Частичное упорядочение не выполняется в контексте определения.Но если / когда вам случится вызвать разрешение перегрузки между двумя функциями путем записи вызова f((int*)0,0) - и в то время, когда компилятор либо пытается собрать объявление кандидата или частично упорядочить их (если доходит до этапа частичного упорядочивания) если в результате получается недопустимое выражение или тип как часть типа функции, SFINAE выручает нас и сообщает нам, что вывод шаблона завершается неудачей (что касается частичного упорядочения, это подразумевает, что один не может быть более специализированным, чем другой, если мы даже не смогли преобразовать шаблон).

Теперь что касается POI - если вы, как и я, убеждены, что преобразованные типы функций должны представлять неявные экземпляры с использованием явно предоставленных списков аргументов шаблона (используя уникально сгенерированные типы) тогда уместны следующие стандартные цитаты:

14.6.4.1/1 Для специализации шаблона функции, специализации шаблона функции-члена или специализации для функции-члена или элемента статических данных шаблона класса, если специализация создается неявно поскольку на него ссылаются из другой специализации шаблона, а контекст, из которого на него ссылаются зависит от параметра шаблона, точка создания экземпляра специализации является точкой создания экземпляра включающей специализации.

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

литб:Поскольку частичное упорядочение - это скорее всего a property of the syntactic form of parameters (i.e "T*" against "T(*)[N]"), я бы проголосовал за внесение изменений в спецификацию (например, "если Q появляется во вложенном спецификаторе имени с определенным идентификатором, именующим тип, тогда тип с именем "Q") Или сказать, что названный тип является другим уникальным типом. This means that in template<typename T> void f(T, typename Const<T>::type*); the argument list is (Q, R*), for example. Same for template<typename T> void f(T*, typename ConstI<sizeof(T)>::type); the arg lisst would be (Q*, R). A similar rule would be needed for non-type parameters, of course. Однако мне пришлось бы подумать об этом и сделать несколько тестовых примеров, чтобы посмотреть, приведет ли это к естественному упорядочению.

Ааа - теперь вы предлагаете возможное решение, которое устраняет двусмысленность в пользу того, чего мы все интуитивно ожидаем - это отдельная проблема, и хотя мне нравится направление, в котором вы движетесь, как и вам, мне тоже пришлось бы немного подумать над этим, прежде чем заявлять о его работоспособности.

Спасибо, что продолжили обсуждение.Я бы хотел, чтобы SO не ограничивал вас только размещением комментариев.

Поскольку вы можете редактировать мои посты, пожалуйста, не стесняйтесь отвечать в самом посте, если это проще.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top