Как стать автором
Обновить

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

Круто. Но все-же очень жаль, что С++ в какой-то момент стал развиваться в таком извращенном направлении. Теперь это не остановить, и на шаблонах пишут уже почти все что угодно вплоть до нетривиальных вычислений времени компиляции и парсеров. К сожалению.
А ведь по сути они задумывались лишь как возможность написания универсальных функций и структур данных, т.е. чтобы не писать например отдельные классы списка для int, float и string.

Добавили бы в язык нормальную рефлексию, нормальные синтаксические макросы, все было бы намного проще.
Языку приходится нести тяжёлый груз совместимости: с Си, с огромным количеством программ, написанных в соответствии с предыдущим стандартом. А новые возможности — это и новые ключевые слова, которые добавляют крайне неохотно, и изменения синтаксиса, которые могут оказаться несовместимыми с тем, что уже есть. А если мы захотим C++, который откинул этот груз, в котором есть мощная поддержка конструкций времени компиляции, то он называется D.
В D тоже все далеко не идеально. Классических синтаксических макросов (как в языке nemerle) там вроде бы нет. Рефлексия какая-то вроде есть, но как-то все бессистемно, по крайней мере у меня сложилось такое впечатление.
А что касается С++, то не вижу никаких проблем с введением новых ключевых слов и новых возможностей.
Да и на старых ключевых словах можно много чего сделать.
С Си полной совместимости давно уже нет.
На шаблонах можно написать всякую чушь, но это не обязательно. Более того, сильно шаблонами страдают разве что в академическом интересе, в продакшн-коде шаблоны именно способ обобщить применение одинаковых алгоритмов над разными типами и не более. Рефлексию планируют добавить, Саттер об этом говорил в каком-то недавнем интервью. А Александреску вообще по моему мнению своими книгами наносит больше вреда, чем пользы — там где можно было бы за полчаса решить задачу каким-нибудь дефайном или скриптом этапа пре-компиляции — люди сидят и днями, неделями городят воздушные замки из шаблонов в шаблонах в шаблонах в шаблонах. Зачем?
А вот на собеседовании каком-нибудь обязательно про эти «шаблоны в шаблонах в шаблонах» спросят:)
В реальном кодинге я использую шаблоны именно так, как предполагалось изначально — для универсальных по отношению к какому-то типу функций и классов. Шаблоны в шаблонах, SFINAE и прочие абстракции ни разу не понадобились. Зато очень часто возникает необходимость в рефлексии, в функциональном программировании (лямбда-функции, замыкания и т.д.), в модулях (система инклудов — это самая большая беда С/С++), и еще пожалуй в каких-то простых мелочах, которые почему-то упустили.
Если посмотреть на boost, то многие из бустовских библиотек могут быть отличным примером того, что должно быть продумано и реализовано на языковом уровне, но не реализовано. Но потребность в этих фичах есть, и в результате сделали такие хитроумные реализации этих возможностей (которые по сути есть костыли для языка программирования, и к тому же не всегда работают корректно). А программисты, компилируя каждый раз буст, по сути вынуждены компилировать каждый раз «внутренности» самого компилятора:)
А что-то сложности какие-то странные. is_same в старом стандарте заменяется на sizeof(t1) == sizeof(t2). declval заменяется на static_cast(0), или другой подходящий cast. Единственная сложность, это выкрутить мозг в нужном направлении. Хотя да, в данном контексте будет посложнее.
Очевидно же что из sizeof(t1) == sizeof(t2) не следует равенство типов.
И что нам даст sizeof(int) == sizeof(unsigned int)? или даже для двух структур с одинаковыми длинами?
При использовании старого стандарта в SFINAE-детекторах сравнивались размеры специально созданных типов, заведомо неравные. Нечто вроде typedef char Yes[1]; typedef char No[2];
Чего-то стали минусовать не разобравшись. sizeof(t1) == sizeof(t2) действительно применимо именно в рассматриваемом случае, когда возвращаемым типам разных функций заданы заведомо разные размеры. Я думаю, именно это имел в виду AxisPod. См. пример в моем комменте, там же про static_cast<T*>(0).
Тип MyString, в котором нет difference_type, при подстановке вызовет ошибку: функция возвращала бы несуществующий тип. Аналогично, конструкция val1 — val2 требует наличия бинарного оператора «минус» и тоже может породить синтаксическую ошибку. Вызов difference с аргументами типа MyString сможет «увидеть» только int-версию функции. Эта единственная версия окажется достаточно подходящей только если в MyString определён оператор преобразования в число. Получается, что шаблонная функция difference проверяет тип аргумента на одновременное выполнение сразу трёх условий: наличие difference_type, наличие оператора вычитания и возможность приведения результата вычитания к типу difference_type (преобразование подразумевается оператором return). Типам, нарушающим хотя бы одно условие, эта перегрузка не видна.


Неправда же. На самом деле SFINAE работает только на этапе разрешения перегрузки — при этом во внимание принимается только сигнатура функции, но не её тело. Поэтому отсутствие оператора вычитания не помешает компилятору _выбрать_ эту перегрузку. После того как она выбрана — будет ошибка компиляции в её теле, но будет уже поздно.

Если же хочется перегрузить difference для типов, имеющих оператор вычитания, то нужно сообщить об этом ограничении в сигнатуре, например так:
template <typename T>
auto difference(T&& x, T&& y) -> decltype(x - y) { return x - y; }
Тьфу-ты, forward забыл.

template <typename T>
auto difference(T&& x, T&& y) -> decltype(std::forward<T>(x) - std::forward<T>(y)) {
  return std::forward<T>(x) - std::forward<T>(y);
}
Не думаю, что оператору вычитания или функции определения разности нужна даже потенциальная возможность изменять свои аргументы.
Ну во-первых раз уж вы делегируем операцию вычитания оператору, то давайте не гадать, нужны ли ей rvalue аргументы или нет — а передавать всё что есть, лишним не будет.

Во-вторых я прямо сейчас могу назвать примеры, когда это может быть нужно. Вычитание векторов или матриц — если один из аргументов rvalue, то можно сделать вычитание in place, не выделяя дополнительную память. А потом _переместить_ из него данные (по сути переместить указатели) в возвращаемый объект.
Спасибо, это важный момент, поправил.
Получается, что в первом примере шаблонная функция difference может быть выбрана для типа, имеющего difference_type, но не имеющего перегруженного оператора вычитания, и будет ошибка при компиляции тела функции?
Да, именно так.
has_foo даст сбой, если у класса есть int foo(int) const
Чтобы такого не происходило, нужно чуть переделать дискриминирующие функции:
struct foo_detector
{
	static auto check(void*) -> void; // раз уж С++11, напишем в постфиксной форме - так красивее и однообразнее :)
	template<class T>
	static auto check(T* p) -> decltype(p->foo(42))*; // указатель на чего бы то ни было отличается от не-указателя

	typedef void* match_type; // но мы ожидаем конкретно void foo(int-compatible), поэтому проверяем на void*
};

// менее громоздкий способ писать метафункции - это наследоваться от готовых
template<class T> struct has_foo : std::is_same<foo_detector::match_type, decltype(foo_detector::check((T*)nullptr))> {};


struct P { void foo(int); };
struct Q0 { };
struct Q1 { void foo(void); };
struct Q2 { int  foo(void); };
struct Q3 { int  foo(int ); };

int main()
{
	std::cout << has_foo<P >::value << std::endl;
	std::cout << has_foo<Q0>::value << std::endl;
	std::cout << has_foo<Q1>::value << std::endl;
	std::cout << has_foo<Q2>::value << std::endl;
	std::cout << has_foo<Q3>::value << std::endl;
}

Боюсь, не уловил вашу мысль насчёт сбоя. has_foo не выдаст ошибки компиляции и произведёт верный ответ (false).
Простите, был невнимателен. Выше были сплошные void… void… void, глаз замылился.
А этот трюк с decltype(.....)* позволяет обнаруживать наличие функции foo с произвольным типом результата.
Только нужно будет проверять !is_same<default_type, decltype(detect(.....))>::value, где default_type — тип результата ловушки default_type detect(...).
Тем самым мы гарантируем, что не будет ложного отказа на сигнатуре с default_type.
В Вашей реализации есть неточность: она будет детектировать сигнатуры не только foo(int), но и foo(long), foo(float) и т.д.

Я предлагаю тестировать на получение указателя на метод с определенной сигнатурой:

template<typename T> struct has_foo
{
private:  // Спрячем от пользователя детали реализации.
    template <typename U, void(U::*pfn)(int) = &U::foo>
    struct detector {char _[2];};

    static char detect(...);  // Статическую функцию и вызывать проще.
    template<typename U> static detector<U> detect(U*);
public:
    static const bool value = sizeof(detect(static_cast<T*>(0))) != sizeof(char);  // Вот видите, готово.
};


Проверяет на точное совпадение сигнатуры void foo(int). Как видите, обошлось стандартом C++03, и все довольно просто.
Я не стал заострять на этом внимание, чтобы не отвлекать от основных мыслей. Удовлетворяют ли foo(unsigned long) и foo(float) требованиям к функции foo, зависит от целей проверки; я исходил из предположения, что они достаточно подходящие.
Кроме того, я хотел показать SFINAE именно в контексте C++11, где можно выразить намерение более прямо. Приём с использованием sizeof менее выразителен, чем прямое сравнение «проброшенного» через detect возвращаемого типа с эталоном, хотя и не менее действенен.
Поделитесь, где бы вы это стали использовать? (проверка наличия определенного метода на этапе компиляции)
Допустим, я делаю шаблон, использующий STL-совместимый контейнер. Если этот контейнер, как вектор, хранит данные одним куском, можно написать оптимизированные варианты процедур, с непосредственным доступом к данным через T* data();. А для deque и прочих делаем обычный вариант с итераторами.
погодитека, вы делаете свой вектороподобный контейнер, и для того что бы узнать что он векторо подобный вам нужен SFINAE?
Нет. Я делаю что-то другое, что хранит данные в контейнере, причём разновидность контейнера можно выбирать. Точно так же, как std::queue или std::stack может хранить данные в разных типах контейнеров.
достаточно работать только с итераторами, они на сколько я знаю адаптированы под тип контейнера. Для вашего случая больше подходит частичная специализация вашей шаблонной структуры для конкретных типов контейнеров.

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

т.е. получился такой типа Python, который к тому же даже в runtime не ругнется на отсутсвие метода.
Итераторы не всегда достаточны. У того же deque итераторы того же класса RandomAccess, что и у вектора, а вот сами данные уже не единым куском лежат. И если оптимизация рассчитана именно на непрерывность, а не на произвольный доступ, то как тут быть?
Ваши странные предположения о порядке использования таких шаблонов делают мне удивляться. Такой шаблон я отдаю кому-то только со словами «он параметризуется STL-совместимым контейнером». И факапов не возникнет, потому что до тех пор, пока контейнер следует интерфейсу, всё будет работать. А если не следует, то это выясняется на этапе компиляции. Но с использованием vector или подобного контейнера будут использоваться оптимизированные варианты процедур, а с другими контейнерами — обычные. И выбор осуществляется автоматически, пользователю об этом даже не надо задумываться. Ему не надо выбирать между шаблонами MyThingOptimized и MyThingGeneric, не надо подставлять лишние булевы константы в параметры… Он указывает, каким контейнером хочет пользоваться (и то только если ему реально нужен не тот, что по умолчанию), а шаблон MyThing сам приспособится. И Питон с рунтаймом тут вообще не при делах.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории