Pull to refresh

Comments 106

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

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

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

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

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

Макросы в C/C++ позволяют делать довольно хитрые и удобные вещи, только они все таки в стороне от шаблонов и дженериков. Вот они все таки ближе к определению текстовый процессор, чем шаблоны.
Хотя бы в том, что дженерик всегда работает со своими типами корректно. Если вдруг вы подставили недопустимый тип Т, например вот так:
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`

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

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

Про «лучшесть» или «хужесть» дженериков в целом судить не берусь, но лично мне ими пользоваться удобнее.
возможность собирать дженерик из типов динамически в рантайме.
И невозможность собирать тип в компайл-тайме?
Ну да. Например, генерация имплементации интерфейса, который прилетает по сети. Ну и другие случаи, например см. статью
Печально, когда ничего не требуется генерить в рантайме. То есть почти всегда.
генерация имплементации интерфейса, который прилетает по сети
Это больше про рефлекшн вообще
Ну, в языках использующих JIT в конечном счете все равно код будет в рантайме генерироваться. Так что ничего печального в раскрытии обобщенных классов в рантайме нет.

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

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

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


template в C++/CLI:


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

generic при этом:


  • может работать только с управляемым кодом;
  • не может быть специализирован;
  • не умеет делать трюки вроде SFINAE;
  • не умеет обращаться к произвольным членам типа-параметра;
  • может быть экспортирован за пределы модуля и даже сборки.
Нет, я их разделил по свойствам.
Вы писали, что в разных имплементациях проблем вы не видите, о сравнении свойств там речи не было.
Та часть языка C++/CLI в которой разрешены generic — компилируется в байт-код, а не в машинный.
Это не имеет значения, если рассматривать их как отдельные инструменты. Ничто не мешает сделать шаблоны поверх JIT-компилируемого кода.

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

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


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

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

Нельзя так просто взять и сделать из произвольного шаблона дженерик.
Давайте назовем это «прекомпилированными дженериками» и тогда станет можно?)
UFO just landed and posted this here
Печально, когда ничего не требуется генерить в рантайме. То есть почти всегда.

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

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

Рефлекшн без информации о дженериках ничего не смог бы сделать. Например я бы не смог реализовать такой метод:
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
UFO just landed and posted this here
UFO just landed and posted this here
Ответил ниже (про лямбду)
UFO just landed and posted this here

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

UFO just landed and posted this here
UFO just landed and posted this here

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


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();

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


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

UFO just landed and posted this here
Возможно. Хотя я и не представляю, как именно, и ни разу не видел на практике. Просветите?
UFO just landed and posted this here
Но это упрощенный пример. Если довести до предела, то пользователь вводит в input корретный C++ код фильтрации сущностей, который транслируется в запрос в БД. Может это несколько надуманный пример, но он не сильно отличается от того, что я на реальном проекте видел.
UFO just landed and posted this here
Ну, известно, что на вход фильтра подается объект типа T произвольной вложенности, а на выходе bool.
Основное преимущество дженерика — он вещь в себе. Если вы написали дженерик и он компилируется — то скорее всего он написан правильно. Поэтому он будет корректно работать с любыми типами, которые подходят под ограничения (если они есть).

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

Разные инструменты решают разные задачи, но в чем-то у них есть область пересечения. Дженерики позволяют решить задачу для произвольного типа Т, темплейты же позволяют писать факториалы времени выполнения, но никто не гарантирует того, что для любого возможного T реализация будет верна. Более того, это в общем случае неверно, ведь вместо типа может быть подставлено и число, и что угодно.
Если вы написали дженерик и он компилируется — то скорее всего он написан правильно. Поэтому он будет корректно работать с любыми типами, которые подходят под ограничения (если они есть).
В этом плане отличий от шаблонов как раз минимум. Ведь ограничения точно так же кроются где-то в обобщенном коде, а компиляция сломается лишь при попытке использования неподходящего клиентского типа.
Давайте так: дженерик без условий where на типах будет работать с ЛЮБЫМИ типами, всегда.

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

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

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


Шарп/раст — не важно. Можно тип 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);
}

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

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

Можно тип T никак не использовать, функционал «object» тут никак не задействован
Вы, как юзер, не знаете о том, используется он внутри или нет.
Насколько я понял, концепты указывают необходимое, но не достаточное условие работоспособности шаблона.
Достаточность — это разве не ограничение «сверху»? В этом случае по тем же принципам работают и дженерики.

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


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


interface IFoo {
    void Foo();
}

void Bar<A>(A a) where a : IFoo => a.Baz();

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

Тогда наличие метода — это необходимое условие. Чем это будет хуже, чем:
static_assert(
    std::is_base_of<IFoo, T>::value,
    "T must implement IFoo" );
?

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


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();
}

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

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

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

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


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

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

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

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


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


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

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

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

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

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

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

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

назовите три примера ошибок в коде повсеместно используемых шаблонных библиотек
UFO just landed and posted this here
UFO just landed and posted this here
А мне ине не нужно знать.

В двух словах:
  • Если не компилируется код с дженериком — проблема в моем коде.
  • Если не компилируется код с темплейтом — проблема может быть где угодно.
А мне ине не нужно знать.
Верно, ибо это гарантируется дизайном языка.

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

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

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


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

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

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

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

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


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

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


template <typename T>
T add(T a, T b){
  return a.fooasgjknasgh1htg781gh73(b);
}
Никакой статический анализ не скажет, что метода fooasgjknasgh1htg781gh73 не существует ни у одного объекта в проекте, и этот темплейт обречен провалиться.
Ну а дженерики-то какие дадут преимущества? Для этого вы объявите интерфейс IFoo и сделаете дженерику where T: IFoo и он точно так же повалится лишь тогда, когда выяснится, что пользовательский тип не имплементит IFoo.

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

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

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

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

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

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

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

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

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

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

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

UFO just landed and posted this here
Спасибо, я правильно понимаю, что это пример из Rust?

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

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


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

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

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


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


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


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


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

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


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


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;
}
А чем вас решения C# и Java не устраивают?
Устраивают, наверно, я просто не пишу на этих языках. Это больше к мысли, чего не хватает шаблонам C++.
То такой вариант уже не валиден:

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

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

Но если речь заходит о возврате умного указателя, то код не скопилируется, так как нарушается ковариантность.
я понял о чем вы. Чтобы с++ начал поддерживать подобное, в стандарте должно появиться понятие «ковариантности» как более жесткое отношение наследования, когда любой объект Derived класса является (бинарно) корректным экземпляром Base класса. А вот теперь вопрос — как это проверить? Чтобы Derived был ковариантен Base он должен как минимум не иметь дополнительных полей (должны быть одного размера) и переопределенного невиртуального деструктора. Указатели/ссылки/умные указатели по идее ковариантны, но с точки зрения корректности опять же всплывает требование на виртуальный ~Base() или не переопределенный ~Derived(). А как сформулировать правило в общем виде?
Для указатели и ссылок это уже работает. В принципе можно было бы добавить оператор «повышения» класса, т.е. получения из наследника экземпляр родителя. Но это, конечно, не общий случай. Для более общего все должно стать «указателем». Тогда и рефлексию можно будет легко добавить и ко/ин-вариантность. Но это кардинально изменило бы язык. Так что как ввести это в C++ оставив его при этом C++ — затрудняюсь представить.
Например вот такой случай:
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 язык.
В целом, да. Я про это и написал, что как такое ввести и оставив C++ в том понимании как он есть — я не представляю.

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


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


struct DerivedContainer {
    Derived1 *derived1;
    Derived2 *derived2;

    operator BaseContainer() const {
        BaseContainer r;
        r.base1 = derived1;
        r.base2 = derived2;
        return r;
    }
}

Спасибо за уточнение, про теминологию. Но в разрезе 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 { //Ошибка
        /*...*/
    }
};

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

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

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


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


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

вот как раз таки концепты позволяют решить в т.ч. и эту проблему.
Пример
// Допустим, определен концепт 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

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

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

if( 4 in ar )
{
}
Эээ, как бы
vector<int> vec;
vec.push_back( 10 );
vec.push_back( 20 );
 
for (int i : vec ) 
{
    cout << i;
}
set<int> myIntSet = { 1, 4, 7, 9 };

if ( myIntSet.find( 4 ) != myIntSet.cend() )
{
}
Вы отличаете языковую поддержку (о чем и сказано в статье) от кривых костылей библиотек?

К тому же я написал про array (если бы вы смогли понять что я написал в коде), а не неудачные примеры в STL.
Что вы понимаете под языковой поддержкой? Оператор «in»?

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

ЗЫ Стандарт С++ включает стандартную библиотеку, так что смело можете считать, что любые контейнеры и алгоритмы из нее — это и есть языковая поддержка.
Пожалуй, я бы еще boost сюда включил, но это уже будет не так честно.
Ээээх… Печаль, что всё это станет доступно с 20 стандартом…
всё-таки «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...); // Проверка типа переменной

В общем, всё самое интересное в статье опущено
Sign up to leave a comment.

Articles