К изменениям лучше готовиться заранее, поэтому предлагаю посмотреть на то, что войдет в стандарт C++20, а именно на концепции.


Статус концепций


Сейчас концепции имеют статус технической спецификации(TS: technical specification): документ их описывающий ISO/IEC TS 19217:2015. Такие документы нужны, чтобы перед принятием нововведений в стандарт языка, эти нововведения были опробованы и скорректированы сообществом С++. Компилятор gcc поддерживает техническую спецификацию концепций в экспериментальном режиме с 2015 года.


Стоит заметить, что концепции из технической спецификации и концепции из текущего черновика С++20 различаются, но не сильно. В статье рассматривается вариант технической спецификации.


Теория


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


Практика


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


template<typename T>
concept bool MyConcept = /* ... */;

Вместо комментария нужно написать constexpr выражение, которое приводится к bool. Это выражение и есть ограничение на аргумента шаблона. Что бы ограничить шаблон концепцией, нужно вместо typename(или class) использовать её название.


Например, для целых чисел:


// (На момент написания статьи подсветка синтаксиса не работала для
//  ключевых слов связанных с концепциями)
#include <type_traits>
template<typename T> // концепция целых чисел
concept bool MyIntegral = std::is_integral<T>::value;

//template <typename T>
template<MyIntegral T>
bool compare (T a, T b) {
    return a < b;
}

void foo () {
    compare (123u, 321u); /// OK
    compare (1.0, 2.0);   /** ОШИБКА: нарушение ограничений концепции MyIntegral
                              (std::is_integral<double>::value = false)
                          */
}

Можно ставить более сложные ограничения, используя требование-выражение(requires-expression). Требование-выражение умеет проверять правильность(well-formed) выражения, возвращаемое значение выражения, наличие типов. Синтаксис хорошо разобран тут.


#include <unordered_set>
#include <vector>

template<typename T>
concept bool MyComparable = requires (T a, T b) {
    a < b;  /// Проверяем, что такое выражение правильно
    { a < b } -> bool; /// Проверяем, что сравнение приводится к типу bool
};

template<MyComparable T>
bool compare (T a, T b) {
    return a < b;
}

void foo () {
    std::vector<int>        vecA = {1, 2, 3}, vecB = {1, 2, 4};
    std::unordered_set<int> setA = {1, 2, 3}, setB = {1, 2, 4};

    compare (vecA, vecB); /// OK
    compare (setA, setB); /** Нарушение ограничений концепции MyComparable
                              std::unordered_set не имеет
                                операции сравнения.
                              требование ( a < b ) не выполнено.
                           */
}

Сортировка


Как же концепции помогут в написании сортировки? Сам алгоритм останется неизменным, но шаблон сортировки можно улучшить с помощью концепций. Рассмотрим такой пример:


#include <algorithm>
struct NonComparable {};
int main () {
    std::vector<NonComparable> vector = {{}, {}, {}, {}, {}, {}, {}, {}};
    std::sort (vector.begin(), vector.end()); // Ошибка
}

Ошибка заключается, в том что у структуры NonComparable нет операции сравнения. Представляете как будет выглядеть ошибка компилятора? Если нет, то загляните под спойлер.


gcc(7.2.1) CentOS
[username@localhost concepts]$ g++ -std=c++17 main.cpp
In file included from /opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algobase.h:71:0,
                 from /opt/rh/devtoolset-7/root/usr/include/c++/7/vector:60,
                 from main.cpp:1:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/predefined_ops.h: In instantiation of ‘constexpr bool __gnu_cxx::__ops::_Iter_less_iter::operator()(_Iterator1, _Iterator2) const [with _Iterator1 = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Iterator2 = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >]’:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:81:17:   required from ‘void std::__move_median_to_first(_Iterator, _Iterator, _Iterator, _Iterator, _Compare) [with _Iterator = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Compare = __gnu_cxx::__ops::_Iter_less_iter]’
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:1921:34:   required from ‘_RandomAccessIterator std::__unguarded_partition_pivot(_RandomAccessIterator, _RandomAccessIterator, _Compare) [with _RandomAccessIterator = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Compare = __gnu_cxx::__ops::_Iter_less_iter]’
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:1953:38:   required from ‘void std::__introsort_loop(_RandomAccessIterator, _RandomAccessIterator, _Size, _Compare) [with _RandomAccessIterator = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Size = long int; _Compare = __gnu_cxx::__ops::_Iter_less_iter]’
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:1968:25:   required from ‘void std::__sort(_RandomAccessIterator, _RandomAccessIterator, _Compare) [with _RandomAccessIterator = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Compare = __gnu_cxx::__ops::_Iter_less_iter]’
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:4836:18:   required from ‘void std::sort(_RAIter, _RAIter) [with _RAIter = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >]’
main.cpp:6:44:   required from here
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/predefined_ops.h:43:23: error: no match for ‘operator<’ (operand types are ‘NonComparable’ and ‘NonComparable’)
       { return *__it1 < *__it2; }
                ~~~~~~~^~~~~~~~
In file included from /opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algobase.h:67:0,
                 from /opt/rh/devtoolset-7/root/usr/include/c++/7/vector:60,
                 from main.cpp:1:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_iterator.h:888:5: note: candidate: template<class _IteratorL, class _IteratorR, class _Container> bool __gnu_cxx::operator<(const __gnu_cxx::__normal_iterator<_IteratorL, _Container>&, const __gnu_cxx::__normal_iterator<_IteratorR, _Container>&)
     operator<(const __normal_iterator<_IteratorL, _Container>& __lhs,
     ^~~~~~~~
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_iterator.h:888:5: note:   template argument deduction/substitution failed:
In file included from /opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algobase.h:71:0,
                 from /opt/rh/devtoolset-7/root/usr/include/c++/7/vector:60,
                 from main.cpp:1:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/predefined_ops.h:43:23: note:   ‘NonComparable’ is not derived from ‘const __gnu_cxx::__normal_iterator<_IteratorL, _Container>’
       { return *__it1 < *__it2; }
                ~~~~~~~^~~~~~~~
и т.д.

Такая маленькая ошибка в коде и такая большая у компилятора. Абыдно, да!?


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


template<typename T>
concept bool MySwappable = requires (T a, T b) {
    std::swap(a, b); // Можно обменивать
};

перемещаемый объект


template<typename T>
concept bool MyMovable = requires (T a) {
    T (std::move(a)); // Можно конструировать перемещением
    a = std::move(a);  // Можно присваивать перемещением

};

итератор случайного доступа


template<typename T>
concept bool MyRAIterator = requires (T it) {
    typename T::value_type; // Есть тип на который указывает итератор
    it++; // Можно работать как с Random Access итератором
    it--;
    it += 2;
    it -= 2;
    it = it + 2;
    it = it - 2;
    { *it } -> typename T::value_type; // Можно разыменовать
};

Когда все простые концепции готовы, можно определить составную концепцию Сортируемого итератора:


template<typename T>
concept bool MySortableIterator =
    MyRAIterator<T> &&                       // Итератор случайного доступа
    MyMovable<typename T::value_type> &&     // Перемещаемый объект
    MyComparable<typename T::value_type> &&  // Сравнимый объект
    MySwappable<typename T::value_type>;     // Обмениваемый объект

С его помощью пишется враппер:


template<MySortableIterator T>
void conceptualSort (T begin, T end) {
    std::sort (begin, end);
}

Если вызывать "концептуальную" сортировку с несравниваемым объектом,


struct NonComparable {};

int main () {
    std::vector<NonComparable> vector = {{}, {}, {}, {}, {}, {}, {}, {}};
    conceptualSort (vector.begin(), vector.end()); // Ошибка
}

то ошибка компиляции будет занимает всего 16 строк:


gcc(7.2.1) CentOS
[markgrin@localhost concepts]$ g++ -std=c++17 -fconcepts main.cpp
main.cpp: In function ‘int main()’:
main.cpp:49:49: error: cannot call function ‘void conceptualSort(T, T) [with T = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >]’
     conceptualSort (vector.begin(), vector.end());
                                                 ^
main.cpp:41:6: note:   constraints not satisfied
 void conceptualSort (T begin, T end) {
      ^~~~~~~~~~~~~~
main.cpp:36:14: note: within ‘template<class T> concept const bool MySortableIterator<T> [with T = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >]’
 concept bool MySortableIterator = MyRAIterator<T> && MyMovable<typename T::value_type> &&
              ^~~~~~~~~~~~~~~~~~
main.cpp:12:14: note: within ‘template<class T> concept const bool MyComparable<T> [with T = NonComparable]’
 concept bool MyComparable = requires (T a, T b) {
              ^~~~~~~~~~~~
main.cpp:12:14: note:     with ‘NonComparable a’
main.cpp:12:14: note:     with ‘NonComparable b’
main.cpp:12:14: note: the required expression ‘(a < b)’ would be ill-formed

Конечно, первые разы все равно не очень просто понять в чем ошибка, но после нескольких "концептуальных" ошибок они начинают читаться за несколько секунд.


Заключение


Конечно, сокращение длины ошибок не единственное преимущество нововведения. Шаблоны станут безопаснее благодаря ограничениям. Код станет более читаемым благодаря именнованным концепциям(самые часто используемые войдут в библиотеку). В целом С++ расширится в своей функциональной(шаблонной) части.

Комментарии (106)


  1. ratijas
    06.02.2018 17:11

    Что ещё осталось в мире, чего нет в крестах?

    А, ну да. В нем всё ещё нет нормальных дженериков — только шаблоны а-ля банальный текстовый препроцессор.


    1. Hokum
      06.02.2018 17:17
      -1

      А в чем преимущество дженериков перед шаблонами? На мой взгляд шаблоны куда более мощный инструмент.
      Да и все что описано в данное статье не столь изящно, но решается обычными шаблонами. Без условно концепции позволят делать все это куда более понятно и короче, так что я «за».


      1. ratijas
        06.02.2018 17:23
        -1

        Мощный, да. Как топор. И такой же примитивный. define тоже мощный — вон какие чудеса из чистой сишки творит.

        PS за минус сорян, случайно вышло :c


        1. Hokum
          06.02.2018 17:52
          -1

          Мне на самом деле интересно в чем вы видите преимущества дженериков, в каких моментах в C++ они стали бы более удобным инструментом, не холивара ради? Ну и прежде хотелось бы понять в чем у них разница. Так как в моем понимании это в общем-то разные реализации одной концепции — обобщенного программирования.

          И дженериками я столкнулся только однажды — при изучении Scala и ловил себя на мысле, что мне их мало. Основной неожиданностью для меня было, что дженерики в Scala не позволяют вызвать произвольный методы, что доступно при работе с шаблонами в C++. Задачи решить можно было, но это требовало задействование других механизмов языка. А если при использовании дженериков воспользоваться указанием, что он должен быть наследником какого-то класса, то тогда уже можно и без них обойтись.

          Из основных плюсов — их можно экспортировать, но за счет этого они получаются очень ограниченными.

          Макросы в C/C++ позволяют делать довольно хитрые и удобные вещи, только они все таки в стороне от шаблонов и дженериков. Вот они все таки ближе к определению текстовый процессор, чем шаблоны.


          1. PsyHaSTe
            06.02.2018 20:08

            Хотя бы в том, что дженерик всегда работает со своими типами корректно. Если вдруг вы подставили недопустимый тип Т, например вот так:

            use std::error::Error;
            
            fn print_error<T: Error>(value: T) {
                unimplemented!();
            }
            
            fn main() {
                print_error(10_i32);
            }
            

            То у вас будет понятное сообщение об ошибке:
            Compiling playground v0.0.1 (file:///playground)
            error[E0277]: the trait bound `i32: std::error::Error` is not satisfied
            --> src/main.rs:8:5
            |
            8 | print_error(10_i32);
            | ^^^^^^^^^^^ the trait `std::error::Error` is not implemented for `i32`
            |
            = note: required by `print_error`

            А не мешанина из кишков темплейта.

            Не говоря про возможность собирать дженерик из типов динамически в рантайме.

            Про «лучшесть» или «хужесть» дженериков в целом судить не берусь, но лично мне ими пользоваться удобнее.


            1. Free_ze
              07.02.2018 00:15

              возможность собирать дженерик из типов динамически в рантайме.
              И невозможность собирать тип в компайл-тайме?


              1. PsyHaSTe
                07.02.2018 00:20

                Ну да. Например, генерация имплементации интерфейса, который прилетает по сети. Ну и другие случаи, например см. статью


                1. Free_ze
                  07.02.2018 08:48

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

                  генерация имплементации интерфейса, который прилетает по сети
                  Это больше про рефлекшн вообще


                  1. mayorovp
                    07.02.2018 10:00

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

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


                    1. Free_ze
                      07.02.2018 11:00

                      в языках использующих JIT в конечном счете все равно код будет в рантайме генерироваться.
                      Ну, например, компиляторы C++/CLI или C# (unsafe), вполне могут полагаться на JIT, так и генерировать нативный код.


                      1. mayorovp
                        07.02.2018 11:02

                        Не могут. Компилятор C# не имеет генерировать нативный код в принципе, а в C++/CLI генериком может быть только управляемый класс…


                        1. Free_ze
                          07.02.2018 11:17

                          Компилятор C# не имеет генерировать нативный код в принципе
                          Да, согласен, тут заблуждался.

                          C++/CLI генериком может быть только управляемый класс…
                          Обратного не утверждалось. Однако как рантаймовые дженерики, так и компайлтаймовые шаблоны там есть, хотя вы их разделили лишь по признаку наличия JIT.


                          1. mayorovp
                            07.02.2018 12:59
                            +1

                            Нет, я их разделил по свойствам.


                            template в C++/CLI:


                            • может работать с любым кодом;
                            • может быть специализирован;
                            • умеет SFINAE;
                            • может обращаться к любым членам типа-параметра;
                            • не может экспортироваться за пределы модуля (экспортироваться могут только его инстансы).

                            generic при этом:


                            • может работать только с управляемым кодом;
                            • не может быть специализирован;
                            • не умеет делать трюки вроде SFINAE;
                            • не умеет обращаться к произвольным членам типа-параметра;
                            • может быть экспортирован за пределы модуля и даже сборки.


                            1. Free_ze
                              07.02.2018 13:03

                              Нет, я их разделил по свойствам.
                              Вы писали, что в разных имплементациях проблем вы не видите, о сравнении свойств там речи не было.


                              1. mayorovp
                                07.02.2018 13:06

                                Та часть языка C++/CLI в которой разрешены generic — компилируется в байт-код, а не в машинный.


                                1. Free_ze
                                  07.02.2018 13:23

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

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


                                  1. mayorovp
                                    07.02.2018 13:25

                                    Ничто не мешает сделать шаблоны поверх JIT-компилируемого кода.

                                    Более того, в языке они есть.


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

                                    Нельзя так просто взять и сделать из произвольного шаблона дженерик.


                                    1. Free_ze
                                      07.02.2018 13:31

                                      Нельзя так просто взять и сделать из произвольного шаблона дженерик.
                                      Давайте назовем это «прекомпилированными дженериками» и тогда станет можно?)


                    1. 0xd34df00d
                      07.02.2018 23:30

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

                      Вот ровно поэтому и непонятен профит дженериков в С++.


                  1. PsyHaSTe
                    07.02.2018 12:14

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

                    В моем случае почему-то это почти всегда. Типичный пример — построение запроса, основанного на пользовательском вводе.

                    Это больше про рефлекшн вообще

                    Рефлекшн без информации о дженериках ничего не смог бы сделать. Например я бы не смог реализовать такой метод:
                    internal static class AsyncRequestProcessorResolver
                    {
                    	private static readonly MethodInfo ExecuteAsync = typeof(IAsyncRequestProcessor).GetRuntimeMethod("ExecuteAsync", new[] {typeof(IRemoteRequest) });
                    	private static readonly MethodInfo GetResultAsync = typeof(IAsyncRequestProcessor).GetRuntimeMethod("GetResultAsync", new[] { typeof(IRemoteRequest) });
                    
                    	public static MethodInfo GetRequestMethod(MethodInfo interfaceMethod)
                    	{
                    		return interfaceMethod.ReturnType.GenericTypeArguments.Length == 0 ? ExecuteAsync : GetResultAsync.MakeGenericMethod(interfaceMethod.ReturnType.GenericTypeArguments);
                    	} 
                    }

                    Если бы у меня собственно не было MakeGenericMethod и interfaceMethod.ReturnType.GenericTypeArguments


                    1. 0xd34df00d
                      07.02.2018 23:32

                      В моем случае почему-то это почти всегда. Типичный пример — построение запроса, основанного на пользовательском вводе.

                      Можно пример?


                      1. PsyHaSTe
                        07.02.2018 23:36

                        Это не моя цитата.


                        1. 0xd34df00d
                          07.02.2018 23:37

                          Тьфу, Ctrl+C не нажался. Спасибо, обновил.


                          1. PsyHaSTe
                            07.02.2018 23:45

                            Ответил ниже (про лямбду)


                1. 0xd34df00d
                  07.02.2018 23:32

                  Интерфейс прилетает по сети? А как вы тогда в вызывающем коде статически типизированно собираетесь с ним работать?


                  1. PsyHaSTe
                    07.02.2018 23:35

                    Имеется ввиду, что он деконструируется на некоторые известные части. Типичный пример, построить лямбду x=>x.Name == "Alex" && x.Gender = Gender.M на основании пользовательского текстового ввода.


                    1. 0xd34df00d
                      07.02.2018 23:37

                      Так это ж каноничное expression templates.


                      1. PsyHaSTe
                        07.02.2018 23:44

                        Можно пример?


                        1. 0xd34df00d
                          08.02.2018 01:41

                          Да, например. Надеюсь, я правильно понял ваш контекст, и аналогия верная.


                          1. PsyHaSTe
                            08.02.2018 02:02

                            Тяжело читать плюсы. Насколько я понял, мы от пользователя получаем только параметры запроса, который выполняем. Я имел ввиду скорее генерацию самого запроса с нуля. У нас на проекте, например, была фильтрация, которая могла иметь произвольную вложенность. Всевозможные фильтры, объединяемые через И-ИЛИ. Весь фильтр целиком по сути имел свойства (де)сериализации из/в пользовательский ввод и умение выполняться в БД. Примера из этой системы не покажу, но например я класс, который генерирует объекты сравнения. Например, пишем так:


                            var zComparer = ZComparer<Test>.New(t => t.A).Add(t => t.B).Add(t => t.C).Add(t => t.D);
                            var comparer = zComparer.ToComparer();

                            На выходе имеем объект типа IComparer с методом compare, который реализован как


                            public int CompareTo(Test x, test y) 
                            {
                               var compA = x.A.CompareTo(y.A);
                               if (compA != 0)
                                  return compA;
                               var compb = x.B.CompareTo(y.B);
                               if (compB != 0)
                                  return compB;   
                               var compC = x.C.CompareTo(y.C);
                               if (compC != 0)
                                  return compC;
                               var compD = x.D.CompareTo(y.D);
                               if (compD != 0)
                                  return compD; 
                               return 0;
                            }

                            Можно ли тут сгенерировать нужный тип на этапе компиляции? Безусловно. Но только потому, что ZComparer<Test>.New(t => t.A).Add(t => t.B).Add(t => t.C).Add(t => t.D); мы знаем на этапе компиляции. Если же у нас немного больше динамики


                            var zComparer = ZComparer<Test>.New();
                            if (userInputA)
                               zComparer= zComparer.Add(t => t.A);
                            if (userInputB)
                               zComparer= zComparer.Add(t => t.B);
                            var comparer = zComparer.ToComparer();

                            То сгенерировать нужный фильтр мы можем только в рантайме.


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


                            1. 0xd34df00d
                              08.02.2018 02:17

                              Этого тоже вполне можно добиться на плюсах.


                              1. PsyHaSTe
                                08.02.2018 02:25

                                Возможно. Хотя я и не представляю, как именно, и ни разу не видел на практике. Просветите?


                                1. 0xd34df00d
                                  08.02.2018 02:32

                                  Каждый следующий Add принимает лямбду/указатель на поле/указатель на функцию и добавляет соответствующий std::function в список, по которому потом пробегает метод CompareTo в цикле. Можно это всё написать в виде кода, в принципе, но, надеюсь, и так примерная мысль понятна.


                                  1. PsyHaSTe
                                    08.02.2018 12:20

                                    Но это упрощенный пример. Если довести до предела, то пользователь вводит в input корретный C++ код фильтрации сущностей, который транслируется в запрос в БД. Может это несколько надуманный пример, но он не сильно отличается от того, что я на реальном проекте видел.


                                    1. 0xd34df00d
                                      08.02.2018 19:05

                                      А сущности-то определены на этапе компиляции?


                                      1. PsyHaSTe
                                        08.02.2018 19:56

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


              1. PsyHaSTe
                07.02.2018 23:43

                Основное преимущество дженерика — он вещь в себе. Если вы написали дженерик и он компилируется — то скорее всего он написан правильно. Поэтому он будет корректно работать с любыми типами, которые подходят под ограничения (если они есть).

                Темплейт же может таить в себе что угодно, и до момента инстанцирования сказать, рабочий ли он — невозможно.

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


                1. Free_ze
                  08.02.2018 00:53

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


                  1. PsyHaSTe
                    08.02.2018 00:56

                    Давайте так: дженерик без условий where на типах будет работать с ЛЮБЫМИ типами, всегда.

                    Могут ли такие гарантии быть у темплейтов?


                    1. Free_ze
                      08.02.2018 11:27

                      Если говорить про .NET, то отсутствие where еще не означает отсутствие ограничений. Там все типы наследуют некую общую функциональность типа Object, кроме того вы не сможете создать объект конструктором по умолчанию и прочие такие вещи.

                      Да, грустно, что автоматически соответствие алгоритма ассертам не проверяется до момента инстанцирования. Но я не исключаю возможности таких проверок, когда появятся концепты. И что тогда?


                      1. PsyHaSTe
                        08.02.2018 12:30
                        +1

                        Ладно, перефразирую: чтобы понять, как будет работать дженерик с типом Т достаточно посмотреть на сигнатуру метода. Если же ошибка в темплейте, единственный способ посмотреть — залезть в него и посмотреть, как он используется внутри. Это совершенно разная сложность.


                        Шарп/раст — не важно. Можно тип T никак не использовать, функционал "object" тут никак не задействован:


                        public T[] CreateArray<T>(int size) => new T[size];

                        в интерфейсе я вижу:


                        public T[] CreateArray<T>(int size) => new T[size];

                        interface IArrayCreator 
                        {
                           public T[] CreateArray<T>(int size);
                        }

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


                        1. Free_ze
                          08.02.2018 12:49
                          -1

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

                          Можно тип T никак не использовать, функционал «object» тут никак не задействован
                          Вы, как юзер, не знаете о том, используется он внутри или нет.


                          1. mayorovp
                            08.02.2018 12:59

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


                            1. Free_ze
                              08.02.2018 13:14

                              Достаточность — это разве не ограничение «сверху»? В этом случае по тем же принципам работают и дженерики.


                              1. mayorovp
                                08.02.2018 13:20

                                В C# — нет, не по тем же. Нельзя обращаться к членам типа-параметра невыводимым из ограничений


                                Вот такой код не скомпилируется, будет ошибка что у типа A не виден метод Baz:


                                interface IFoo {
                                    void Foo();
                                }
                                
                                void Bar<A>(A a) where a : IFoo => a.Baz();

                                В то же время аналогичный код на шаблонах в C++ будет компилироваться без ошибок до тех пор пока шаблоном не попытаются воспользоваться.


                                1. Free_ze
                                  08.02.2018 13:44

                                  Тогда наличие метода — это необходимое условие. Чем это будет хуже, чем:

                                  static_assert(
                                      std::is_base_of<IFoo, T>::value,
                                      "T must implement IFoo" );
                                  ?


                                  1. mayorovp
                                    08.02.2018 13:48

                                    Вот смотрите, я пишу:


                                    struct IFoo {
                                        virtual void Foo() = 0;
                                    };
                                    
                                    template<typename A> void Bar(A &a) {
                                        static_assert(std::is_base_of<IFoo, A>::value, "A must implement IFoo" );
                                        return a.Baz();
                                    }

                                    И этот код компилируется без ошибок. Вот в этом "без ошибок" и проблема: в реализации серьезная ошибка, но компилятор ее не видит.


                                    1. Free_ze
                                      08.02.2018 14:08

                                      Здесь нет ошибки. Это статический полиморфизм — очень мощная возможность C++.

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


                                      1. mayorovp
                                        08.02.2018 14:15

                                        Это называется не "статический полиморфизм", а "утиная типизация".


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

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


                                        1. Free_ze
                                          08.02.2018 14:17

                                          Что мешает вам здесь говорить об этом, как о статическом полиморфизме?


                                          1. mayorovp
                                            08.02.2018 14:22

                                            То, что это не статический полиморфизм.


                                            Статический полиморфизм — это сама возможность инстанцировать шаблон конкретным типом без виртуальных вызовов в рантайме.


                                            А доступ к методу Baz когда нигде не описано что такой метод у типа A есть — это именно что утиная типизация.


                                            1. Free_ze
                                              08.02.2018 14:29
                                              -1

                                              Статический полиморфизм — это сама возможность инстанцировать шаблон конкретным типом без виртуальных вызовов в рантайме.
                                              У нас нет такой возможности?) Мы не можем позвать методы с подходящими сигнатурами от объектов типов, не состоящих в родственной связи?


                                              1. mayorovp
                                                08.02.2018 14:38

                                                Где я это писал? Вы вообще отличаете утверждения "вызов a.Baz() — это не статический полиморфизм" и "у нас нет статического полиморфизма"?


                                                1. Free_ze
                                                  08.02.2018 14:43

                                                  Хорошо, тогда вы писали:

                                                  нигде не описано что такой метод у типа A есть
                                                  А как же сам шаблон? Гарантия того, что этот метод есть — это ошибка инстанцирования в противном случае.


                                                  1. mayorovp
                                                    08.02.2018 14:47

                                                    Вот и приходим к тому с чего начали — чтобы увидеть реальный контракт шаблона надо изучить всю его реализацию, а концепты указывают необходимое, но не достаточное условие.


                                                    1. Free_ze
                                                      08.02.2018 15:27

                                                      Вот и приходим к тому с чего начали
                                                      Мы продолжаем обсуждать термин «статический полиморфизм» в отношении шаблонов, вы мне так и не объяснили, почему же вызов метода по имени из объекта параметра-типа — это не статический полиморфизм?

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


                                                      1. mayorovp
                                                        08.02.2018 15:28

                                                        Если весь контракт будет в концептах и других ограничениях, то это не потребуется.

                                                        … но вы никогда не сможете быть уверены в этом. Или вы живете в мире, где программисты никогда не ошибаются?


                                                        1. Free_ze
                                                          08.02.2018 15:35

                                                          Возможно, в каком-нибудь C++23 сделают проверку на соответствие шаблона концепту. Хотелось бы верить) Но эти ошибки просто ловить и легко фиксить, так что не думаю, что это большая проблема.


                                                          1. mayorovp
                                                            08.02.2018 15:38

                                                            Это не большая проблема только до тех пор пока обе части кода — объявление шаблона и его использование — пишутся одним разработчиком.


                                                            1. Free_ze
                                                              08.02.2018 15:55

                                                              Повсеместное использование статических анализаторов — это реальность сегодняшнего дня. Подозреваю, что автоматизировать такую проверку будет не самой сложной задачей для них.


                                                        1. Antervis
                                                          08.02.2018 15:58

                                                          Или вы живете в мире, где программисты никогда не ошибаются?

                                                          назовите три примера ошибок в коде повсеместно используемых шаблонных библиотек


                                                          1. 0xd34df00d
                                                            08.02.2018 19:08

                                                            Не совсем то, но весьма распространённое отсутствие SFINAE friendliness, например.


                                        1. 0xd34df00d
                                          08.02.2018 19:06

                                          Это называется не «статический полиморфизм», а «утиная типизация».

                                          Одно другому не мешает. Утиность типизации вообще можно рассматривать как деталь реализации.


                          1. PsyHaSTe
                            08.02.2018 13:00
                            +1

                            А мне ине не нужно знать.

                            В двух словах:

                            • Если не компилируется код с дженериком — проблема в моем коде.
                            • Если не компилируется код с темплейтом — проблема может быть где угодно.


                            1. Free_ze
                              08.02.2018 13:22
                              -1

                              А мне ине не нужно знать.
                              Верно, ибо это гарантируется дизайном языка.

                              Если не компилируется код с темплейтом — проблема может быть где угодно.
                              Какие вы видите оганичения возможности вычислить соответствие шаблона концепту на этапе компиляции? Аналогично тому, как компилятор контролирует дженерики.


                              1. PsyHaSTe
                                08.02.2018 13:32

                                Верно, ибо это гарантируется дизайном языка.

                                При чем тут дизайн языка? В любом языке зная тип T можно создать массив T[]. Тут не используется ни одного метода или свойства Object.


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

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


                                1. Free_ze
                                  08.02.2018 13:36

                                  Тут не используется ни одного метода или свойства Object.
                                  Юзер не может это знать, он видит сигнатуру.

                                  Насколько я знаю, темплейты вообще не компилируются, а инстанцируются по месту использования.
                                  Хорошо, конкретизирую: на этапе статического анализа.


                                  1. PsyHaSTe
                                    08.02.2018 13:39

                                    Юзер не может это знать, он видит сигнатуру.

                                    Ну ок, допустим. Я не согласен, но допустим. Какая нам разница? Любой объект любого типа можно подставить вместо T? Можно. Что еще нужно?


                                    Хорошо, конкретизирую: на этапе статического анализа.

                                    Никакой статический анализ не скажет, что метода fooasgjknasgh1htg781gh73 не существует ни у одного объекта в проекте, и этот темплейт обречен провалиться.


                                    template <typename T>
                                    T add(T a, T b){
                                      return a.fooasgjknasgh1htg781gh73(b);
                                    }


                                    1. Free_ze
                                      08.02.2018 13:55

                                      Никакой статический анализ не скажет, что метода fooasgjknasgh1htg781gh73 не существует ни у одного объекта в проекте, и этот темплейт обречен провалиться.
                                      Ну а дженерики-то какие дадут преимущества? Для этого вы объявите интерфейс IFoo и сделаете дженерику where T: IFoo и он точно так же повалится лишь тогда, когда выяснится, что пользовательский тип не имплементит IFoo.

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

                                      То есть нужен аналог сишарпового where, только с более широкими возможностями ограничений.


                                      1. mayorovp
                                        08.02.2018 14:03

                                        Ну а дженерики-то какие дадут преимущества? Для этого вы объявите интерфейс IFoo и сделаете дженерику where T: IFoo и он точно так же повалится лишь тогда, когда выяснится, что пользовательский тип не имплементит IFoo.

                                        Ничего подобного. Дженерик повалится тогда, когда окажется что в интерфейсе IFoo нет метода fooasgjknasgh1htg781gh73.


                                      1. PsyHaSTe
                                        08.02.2018 14:08

                                        Ну а дженерики-то какие дадут преимущества?

                                        В том, что будет ошибка "не реализован интерфейс IFoo", а не "не найден метод fooasgjknasgh1htg781gh73" Преимущество очевидно тогда, когда у вас темплейт вызывает другие темплейт-функции, и падает где-то в глубине нижних уровней из-за какого-то непонятного несоответствия.


                                        1. Free_ze
                                          08.02.2018 14:10

                                          С этим я согласен отчасти, ибо грамотные статические проверки и это могут покрыть и дать более вразумительный текст ошибки. Да, это сложно писать. Но концепты это исправят. И что тогда?)


                                          1. PsyHaSTe
                                            08.02.2018 15:28

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


                                1. Antervis
                                  08.02.2018 13:46

                                  Во-первых, существуют проверки в виде static_assert/enable_if/SFINAE — даже в с++14 можно проверить соответствие типа требованиям шаблона.
                                  Во-вторых, функционал шаблона может зависеть от свойств типа-параметра. vector от unique_ptr не поддерживает копирование, т.к. unique_ptr не поддерживает копирование
                                  В третьих, концепты и позволят накладывать все необходимые ограничения на тип. Ошибка возникнет в момент инстанцирования шаблона, и укажет именно на то требование к типу, которое не выполняется

                                  Никакой статический анализ не скажет, что метода fooasgjknasgh1htg781gh73 не существует ни у одного объекта в проекте, и этот темплейт обречен провалиться.

                                  как раз то о чем я говорил:
                                  requires (T a, T b) { a.fooasgjknasgh1htg781gh73(b) } 
                                  

                                  На enable_if сложнее, но тоже реализуемо


                                  1. mayorovp
                                    08.02.2018 13:50

                                    Ошибка возникнет в момент инстанцирования шаблона, и укажет именно на то требование к типу, которое не выполняется

                                    Так в том-то и проблема, что все требования к типу выполняются, но ошибка компиляции все равно возникнет. Потому что ошибка — в самом шаблоне.


                                    1. Antervis
                                      08.02.2018 14:08

                                      во-первых, возможно, этот шаблон собираются использовать только с типами, для которых определен T::baz(), отсутствующий в IFoo. Иначе метод можно реализовать и без шаблона.
                                      во-вторых, посмотрите на это вот с какой стороны: за корректность библиотеки и проверку входных параметров/данных отвечает библиотека.


                                      1. mayorovp
                                        08.02.2018 14:20

                                        во-вторых, посмотрите на это вот с какой стороны: за корректность библиотеки и проверку входных параметров/данных отвечает библиотека.

                                        Отвечает-то библиотека, вот только сыпятся ошибка компиляции не на авторов библиотеки, а на ее пользователей почему-то. Это и неправильно.


                1. 0xd34df00d
                  08.02.2018 01:44

                  Более того, это в общем случае неверно, ведь вместо типа может быть подставлено и число, и что угодно.

                  Или потому, что
                  template<typename T>
                  struct Meh {};
                  
                  template<typename T>
                  struct Meh<std::vector<T>>
                  {
                      static_assert(std::is_same_v<T, struct Dummy>);
                  };
                  


                  Кстати, в этом коде, возможно, UB. Я недостаточно language lawyer, чтобы об этом сказать.

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


            1. Hokum
              07.02.2018 00:45

              Спасибо, я правильно понимаю, что это пример из Rust?

              Я как-то не задумывался, что дженерики работают и в рантайме. Тогда для некоторых задач, они становятся удобнее шаблонов, в частности сохранения отношения наследования, или инвертирование его.


              1. PsyHaSTe
                07.02.2018 01:38

                Ну, в расте райнтаймовой поддержки на данный момент нет, т.к. раст все же обычно имеет зависимости на уровне исходников.


                А вот в том же шарпе да, есть полная поддержка со стороны среды. И например List<int> и List<string> там разные типы, и попытка засунуть одно в другое приведет к ошибке времени выполнения, даже если на этапе компиляции там все было ок.


              1. mayorovp
                07.02.2018 08:45

                Что вы понимаете под «сохранением отношения наследования»?


                1. Hokum
                  07.02.2018 13:37

                  Пусть есть три класса: шаблонный класс Templ, класс Base и Derived, где является наследником B, то Templ будет являться наследником Templ. Т.е. Templ можно будет использовать везде, где ожидается Templ.
                  Такое поведение не может быть общим, но с дженериками это можно реализовать.


                  1. mayorovp
                    07.02.2018 13:43

                    У вас угловые скобки потерялись...


                    Нет, в общем случае Templ<Derived> нельзя использовать в качестве Templ<Base>, как и наоборот. Если бы было можно — это привело бы к многочисленным нарушениям LSP и просто ошибкам в рантайме.


                    То, что вы просите, в языке C# работает только для обобщенных интерфейсов, которые отмечены как ковариантные (это накладывает на интерфейс дополнительные требования на этапе компиляции).


                    В Java ковариантных интерфейсов нет, но зато можно писать конструкции вида Templ<? extends Base> (такой класс "теряет" свои нековариантные методы) — и к вот такому типу действительно можно неявно привести Temp<Derived>.


                    PS вот тут я совсем недавно пример приводил: https://habrahabr.ru/post/348286/#comment_10654282


                    1. Hokum
                      07.02.2018 15:09

                      Про угловые скобки — спасибо, не заметил.


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


                      template<typename T>
                      class List {
                        //...
                        template<typename T1, typename TMain = find_main<T, T1>>
                        List<TMain> add(TMain item);
                        //...
                      };
                      
                      var empty = new List<Circle>; //пустой список
                      var circles = empty.add(new Circle()); //список Circle
                      var shapes = circles.add(new Rectange()); //а здесь уже List<Shape>

                      Но больше пользы будет при работе в C++ со smart_ptr, в части возвращения значений.


                      Например такой вариант сейчас валиден:


                      struct Interface;
                      
                      struct Factory {
                        virtual Interface* create() = 0;
                      };
                      
                      struct ImplInterface: Intreface {};
                      
                      struct SomeFactory: Factory {
                        ImplInterface* create() override;
                      };

                      То такой вариант уже не валиден:


                      struct Interface;
                      
                      struct Factory {
                        virtual uniqie_ptr<Interface> create() = 0;
                      };
                      
                      struct ImplInterface: Intreface {};
                      
                      struct SomeFactory: Factory {
                        uniqie_ptr<ImplInterface> create() override; //Ошибка
                      };

                      Иногда такое удобно. Но для этого еще потребуется такая перегрузка:


                      struct Base1 {};
                      struct Derived1: Base1 {};
                      
                      struct Base2 {};
                      struct Derived2: Base2{};
                      
                      struct BaseInterface {
                        virtual Base1 foo (Derived2);
                      }
                      
                      struct DerivedInterface {
                        Derived1 foo (Base2) override;
                      }


                      1. mayorovp
                        07.02.2018 15:11

                        А чем вас решения C# и Java не устраивают?


                        1. Hokum
                          07.02.2018 15:33

                          Устраивают, наверно, я просто не пишу на этих языках. Это больше к мысли, чего не хватает шаблонам C++.


                      1. Antervis
                        07.02.2018 18:08

                        То такой вариант уже не валиден:

                        Из вашего примера не понятно, какая именно из четырех ваших ошибок связана с недопониманием. Убедитесь, что Interface определен и имеет виртуальный деструктор.


                        1. Hokum
                          07.02.2018 18:36

                          А причем тут недопонимание? И виртуальный деструктор тут совсем не причем. Речь идет о ковариантности, а не о том будет ли вызван деструктор наследников или нет.

                          Просто иногда удобно, когда в базовом классе метод возвращает, например, указатель на тип Q, а в классе наследнике метод перегружен и возвращает указатель на тип W — наследника Q. Таким образом когда работаешь с классом наследником, в той части программы, которая про него знает, можно работать с указателем на W, а общая часть, которая не знает о наследнике работает только с указателем на Q.

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


                          1. Antervis
                            07.02.2018 22:18

                            я понял о чем вы. Чтобы с++ начал поддерживать подобное, в стандарте должно появиться понятие «ковариантности» как более жесткое отношение наследования, когда любой объект Derived класса является (бинарно) корректным экземпляром Base класса. А вот теперь вопрос — как это проверить? Чтобы Derived был ковариантен Base он должен как минимум не иметь дополнительных полей (должны быть одного размера) и переопределенного невиртуального деструктора. Указатели/ссылки/умные указатели по идее ковариантны, но с точки зрения корректности опять же всплывает требование на виртуальный ~Base() или не переопределенный ~Derived(). А как сформулировать правило в общем виде?


                            1. Hokum
                              08.02.2018 00:24

                              Для указатели и ссылок это уже работает. В принципе можно было бы добавить оператор «повышения» класса, т.е. получения из наследника экземпляр родителя. Но это, конечно, не общий случай. Для более общего все должно стать «указателем». Тогда и рефлексию можно будет легко добавить и ко/ин-вариантность. Но это кардинально изменило бы язык. Так что как ввести это в C++ оставив его при этом C++ — затрудняюсь представить.


                              1. Antervis
                                08.02.2018 06:31

                                Например вот такой случай:

                                struct Base1 { virtual ~Base1(); /*...*/ };
                                struct Derived 1 : Base1 { /*...*/ };
                                struct Base2 { virtual ~Base2(); /*...*/ };
                                struct Derived 2 : Base2 { /*...*/ };
                                
                                struct BaseContainer {
                                    Base1 *base1;
                                    Base2 *base2;
                                }
                                
                                struct DerivedContainer {
                                    Derived1 *derived1;
                                    Derived2 *derived2;
                                }
                                

                                DerivedContainer ковариантен BaseContainer, хотя не является (умным) указателем/ссылкой и даже его не наследует.

                                Нельзя «всё сделать указателем». Это 0-cost abstraction язык.


                                1. Hokum
                                  08.02.2018 08:39

                                  В целом, да. Я про это и написал, что как такое ввести и оставив C++ в том понимании как он есть — я не представляю.


                                1. mayorovp
                                  08.02.2018 08:49

                                  Вы немного путаетесь с терминологией. Ковариантность — это свойство параметризованного типа (шаблона или дженерика) в отношении одного из своих параметров, но никак не отношение двух классов.


                                  Что же до вашего случая — тут как раз все очень просто:


                                  struct DerivedContainer {
                                      Derived1 *derived1;
                                      Derived2 *derived2;
                                  
                                      operator BaseContainer() const {
                                          BaseContainer r;
                                          r.base1 = derived1;
                                          r.base2 = derived2;
                                          return r;
                                      }
                                  }


                                  1. Hokum
                                    08.02.2018 10:25

                                    Спасибо за уточнение, про теминологию. Но в разрезе C++ это почти соответствует такому шаблонному коду:


                                    template<typename T1, typename T2>
                                    struct Cont
                                    {
                                        T1* field1;
                                        T2* field2;
                                    };
                                    
                                    using BaseContainer = Cont<Base1, Base2>;
                                    using DerivedContainer = Cont<Derived1, Derived2>;

                                    И вроде они могли бы быть коварианты, но увы.


                                    Можно даже было бы написать обощенный оператор приведения.


                                    template<typename T1, typename T2>
                                    struct Cont
                                    {
                                        T1* field1;
                                        T2* field2;
                                    
                                        template<typename B1, typename B2,
                                            typename = enable_if_t<
                                                is_base_of<B1, T1>::value
                                                && is_base_of<B2, T2>::value>>
                                        operator Cont<B1, B2>()
                                        {
                                        Cont<B1, B2> c;
                                        c.field1 = field1;
                                        c.field2 = field2;
                                        return c;
                                        }
                                    };

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


                                    struct BaseStorage
                                    {
                                        virtual BaseContainer foo() {
                                            /*...*/
                                        }
                                    };
                                    
                                    struct DerivedStorage: BaseStorage
                                    {
                                        DerivedContainer foo() override { //Ошибка
                                            /*...*/
                                        }
                                    };

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


                                    1. mayorovp
                                      08.02.2018 10:35

                                      Не беспокойтесь, в C# и Java так тоже нельзя делать :-)


                                    1. mayorovp
                                      08.02.2018 11:37

                                      Теперь о недостатках вашего решения.


                                      1. Вложенный контейнер работать не будет:
                                        Cont<Derived1, Cont<Derived2, Derived3>> нельзя привести к Cont<Base1, Cont<Base2, Base3>>;


                                      2. dynamic_cast тоже в такой реализации не работает


                                      1. Hokum
                                        08.02.2018 12:28

                                        Да и просто неявное приведение указателей не будет работать. Причем с п.1 еще можно попробовать побороться добавив еще несколько шаблонов, может быть, даже получится решить для общего случая, но боюсь, что в шаблонах можно будет утонуть. :) А вот с неявным приведением указателей сделать точно ничего не удастся.


            1. iyemelyanov
              07.02.2018 09:31

              Действительно на Rust-е это выглядит весьма естественно и вывод компилятора на порядок читаемей!


            1. Antervis
              07.02.2018 11:21

              А не мешанина из кишков темплейта.

              вот как раз таки концепты позволяют решить в т.ч. и эту проблему.
              Пример
              // Допустим, определен концепт Error
              template <typename T>
              concept bool Error = std::is_error_condition_enum<T>::value;
              
              void print(Error e) {
                  cout << error_condition(e).message() << endl;
              }
              
              print(std::errc::argument_out_of_domain); // "Numerical argument out of domain"
              // print(5); // ошибка компиляции, текст ниже
              

              ../src/main.cpp: In function ‘int main()’:
              ../src/main.cpp:211:12: error: cannot call function ‘void print(auto:2) [with auto:2 = int]’
                   print(5);
                          ^
              ../src/main.cpp:181:6: note:   constraints not satisfied
               void print(Error e) {
                    ^~~~~
              ../src/main.cpp:179:14: note: within ‘template<class T> concept const bool Error<T> [with T = int]’
               concept bool Error = std::is_error_condition_enum<T>::value;
                            ^~~~~
              ../src/main.cpp:179:14: note: ‘std::is_error_condition_enum<int>::value’ evaluated to false
              


    1. BalinTomsk
      06.02.2018 20:16

      Pабота с множествами.

      auto ar[] = {1,4,7,9};

      if( 4 in ar )
      {
      }


      1. zorge_van_daar
        06.02.2018 20:40

        Эээ, как бы

        vector<int> vec;
        vec.push_back( 10 );
        vec.push_back( 20 );
         
        for (int i : vec ) 
        {
            cout << i;
        }


      1. Free_ze
        07.02.2018 11:37

        set<int> myIntSet = { 1, 4, 7, 9 };
        
        if ( myIntSet.find( 4 ) != myIntSet.cend() )
        {
        }


        1. BalinTomsk
          07.02.2018 16:55

          Вы отличаете языковую поддержку (о чем и сказано в статье) от кривых костылей библиотек?

          К тому же я написал про array (если бы вы смогли понять что я написал в коде), а не неудачные примеры в STL.


          1. Free_ze
            07.02.2018 17:16

            Что вы понимаете под языковой поддержкой? Оператор «in»?

            я написал про array
            Нет, вы писали про множества) Если там будет std::array, то это мало что изменит.

            ЗЫ Стандарт С++ включает стандартную библиотеку, так что смело можете считать, что любые контейнеры и алгоритмы из нее — это и есть языковая поддержка.
            Пожалуй, я бы еще boost сюда включил, но это уже будет не так честно.


  1. Profi_GMan
    06.02.2018 22:13

    Ээээх… Печаль, что всё это станет доступно с 20 стандартом…


  1. Antervis
    07.02.2018 09:53

    всё-таки «concept» практически всегда переводят как «концепт» а не «концепция».

    template<typename T>
    concept bool MyComparable = requires (T a, T b) {
        // Эта проверка не нужна, т.к. корректность операции проверяется строкой ниже
        a < b;
        // Проверка: "операция a < b определена и её результат контекстуально приводится к bool"
        { a < b } -> bool;
    };
    

    Очень много реального сахара не описано (подробнее можно глянуть здесь). Пример:

    template <typename T>
    concept bool ConstrainedType = ...;
    
    void func(ConstrainedType c); // короткая запись
    
    template <ConstrainedType T>
    void func(T); // Расширенная запись
    
    template <typename T>
        requires ConstrainedType<T>
    void func(T); // полная запись
    

    Полная запись позволяет накладывать ограничения на функцию без объявления нового концепта (bb enable_if). Или, например:
    void func(std::vector<ConstrainedType> v); // Проверка типа - аргумента шаблона
    ConstrainedType func(auto a); // Проверка типа возвращаемого значения
    ConstrainedType val = func(args...); // Проверка типа переменной
    

    В общем, всё самое интересное в статье опущено