Pull to refresh

Comments 65

Картинка с чуваком которого выкидывают из окна:
— Итак, какой сделаем новый сервис пак?
— Поддержка COBOL!
— Новые фичи из С++17!
— А может быть более полная поддержка С++11 в 2010 и 2013 студии?
* выкидывание из окна *
:)
Да ладно, посмотрите список остальных фич — там десяток фиксов именно для С++11.
так в 13 уже есть инициализация полей, а с обновлением который упомянут в начале появятся и constexpr.
Ну вот же из анонса:
"-constexpr (note: except for constructors, so the CTP won’t support literal types yet)"
MS молодцы, все это давно уже есть в шарпе, читая все это возникает дежавю.
Это уже 40 лет есть в Smalltalk. Возникает дежавю.
Самое интересное, что в самом Smalltalk-е этого нет. Но есть в библиотеке. На самом деле, есть континуации — и тоже не в языке, в библиотеке. А через континуации легко делаются сопрограммы. (Впрочем, можно и без континуаций — через процессы.)

Так вот, на что хочу обратить внимание:, когда кому-то понадобились континуации, он просто их взял и сделал сам — без внесений изменений в язык! Причем сделал все это, уйдя с Ruby, так как не смог на тот момент добиться внесения тех самых континуаций в язык — реализовать их как библиотеку было никак (не знаю, что-то изменилось с тех пор? было это уже лет 10 если не 15 назад). В общем, выводы делайте сами… :)
Ну это само собой, на то он и Smalltalk. А так, что континуации, что транзакционная память, что ленивые вычисления — реализуются одинаково легко, буквально в 50 строк, чем мне это и нравится.

P.S.: Уже практически написаны и скоро выйдут еще 2 мои статьи по сабжу :)
Со Smalltalk, увы, не доводилось работать. Если это настолько мощный язык — почему он не в числе основных, используемых для разработки бизнес-приложений? Просто любопытно.
Он использовался и до сих пор используется в областях, где необходимо в одной системе объединить огромное количество бизнес логики. Причем, наращивание объемов и функциональности системы, на удивление, не приводит к усложнению кода. Грубо говоря, сложность восприятия растет линейно в зависимости от объемов кодовой базы.

В свое время Smalltalk был вытеснен Java и, как многие считают, это вытеснение было целенаправленным и, по сути своей, политическим решением.

Вообще, если интересно, можете посмотреть лекцию на youtube, где Роберт Мартин рассказывает о том, почему Smalltalk ушел со сцены, и предостерегает сообщество Ruby (во многом, идейного наследника Smalltalk) от подобной участи.
… Только, пожалуйста, не воспринимайте то, что дядя Боб там говорит очень всерьез. Он извращает ситуацию в угоду развлекательности и продвижению «своих» «идей» о программировании (первое подчинено второму, разумеется).

Ситуация со Smalltalk довольно сложна, и там всего намешано. Конечно, Smalltalk не идеален, но он менее неидеален, чем современный mainstream. Причина его провала не в этом. Да и о полном и окончательном провале говорить пока что рано — «я так думаю!»). Идеи, заложенные в Smalltalk, пробираются к людям разными путями, иногда очень медленно, и почти всегда сначала в очень перевранном виде. Но находят. Так что, вполне вероятно, мы еще встретимся со Smalltalk либо непосредственно, либо в новом воплощении. … В общем, закругляюсь, ибо оффтопик получается.
Да понятно. Тем не менее, это тоже способ обратить внимание ответственности на это дело. По крайней мере, те моменты, которые он упоминает, достойны внимания.
А можете пояснить, почему конструкцию с await/async называют resumable? Там же ничего не приостанавливается, нет?

А есть какая-то информация на тему того, как будет работать yield? Что будет происходить с контекстом текущего треда?

Как это вообще будет внутри работать?
Я подозреваю что yield будет работать как в C# но немного иначе, что-то вроде

auto seq = range(0,100);
do
{
int i = seq.next();
// do smth with i
}while(i < 100);
Дело в том, что C# (как и питон) — управляемый язык. На нативном C++ реализация будет сложнее.
Почему? yield в C# реализуется очень просто: для метода создается класс, в котором есть поля для всех локальных переменных, а также поля StateId и Current. В начало кода метода вставляется блок-диспетчер наподобие следующего:

switch(StateId)
{
    case 1: goto YIELD_1; break;
    case 2: goto YIELD_2; break;
    ...
}

Не вижу никаких причин, почему аналогичную конструкцию сложно будет реализовать на неуправляемом языке.
А что происходит с регистрами? А со стеком?
А я правильно понимаю, что у нас внезапно все переменные вместо стека начинают в куче выделяться?
Не все, а только те, которые в resumable-функции. Это один из вариантов реализации, как писалось в статье, пока не понятно, стоит ли реализовывать этот так, или придумывать что-то еще.
Я бы воспринимал yield скорее как синтаксический сахар.
Представьте, что есть следующий код: (пример на C#, но думаю человеку знакомому с C++ будет достаточно понятно)

public IEnumerable<int> Enumerate()
{
	yield return 1;
	yield return 2;
}

Он трансформируется в достаточно длинную портянку:

public IEnumerable<int> Enumerate()
{
	return new __InnerClass1();
}

class __InnerClass1 : IEnumerable<int>
{
	public int Current { get; private set; }
	private int StateId;
	
	public bool MoveNext()
	{
		switch(StateId)
		{
			case 0: goto YIELD_1; break;
			case 1: goto YIELD_2; break;
			default: return false;
		}
		
		YIELD_1:
		Current = 1;
		StateId = 1;
		return true;
		
		YIELD_2:
		Current = 2;
		StateId = 2;
		return false;
	}
}

Все локальные переменные, требуемые между вызовами yield, должны быть реализованы в виде полей класса __InnerClass1, а соответственно да, скорее всего будут выделяться в куче, а не на стеке. Разумеется, на скорости выполнения это сказывается негативно — уже хотя бы потому, что каждый yield вызывает переключение контекста. С другой стороны, итераторы экономят память: с их помощью можно описывать бесконечные последовательности значений, или просто писать понятный и читаемый код, не критичный к скорости выполнения.
new __InnerClass1 будет создан в куче со всеми вытекающими — нехватка памяти, исключения, необходимость следить за исключениями и прочее.
Это кстати немного напрягает, что появляются такие неявности запрятанные за обычным кодом. Еще немного непонятно насколько все это действительно будет работать на практике (не пришлось использовать async/await C#), в часности интересно такая вещь: если мы вызываем async функцию внутри своей, свою функцию надо тоже объявить как async (либо заблокироваться и ждать окончания). Получается async «заражает» всю цепочку вызовов наверх.
Не обязательно. Компилятор может объявить его как struct и объявлять в качестве локальной переменной на стеке в том методе, где был вызван итератор, тем самым заинлайнив его. Вариант нехватки памяти на объект такого типа кажется мне примерно настолько же вероятным, как ситуация, когда у вас вдруг переполнится стек при попытке аллоцировать на нем локальную переменную.

Опять же, серебряной пули не бывает. yield и async-await не вносят в язык ничего такого, что раньше вообще было невозможно реализовать. Они просто добавляют удобный и легковесный синтаксис, за который, как за любую обертку высокого уровня, придется платить производительностью. Никто не заставляет вас использовать эти возможности в вашем коде, если вы выжимаете из него последние байты и миллисекунды, но появление таких возможностей в языке — это всегда приятно.
Наверное потому, что оригинальная функция прекращает свою работу, «подписывая» future на некую лямбду с остальным кодом. Он уже будет вызван другой стороной по мере появления значения и тем самым функция «продолжится».
resumable — это ведь не «приостановка», это «возобновление». Работа функции, дёрнувшей через await другую функцию, возобновляется после выполнения этой другой функции.
1. Можно войти в тот же самый стек фрейм (не путать с другим стек фреймом для той же самой функции) даже после выхода из этой функции
2. Точно так же как и __async/__await, собственно yield довольно тривиально пишется если есть __await (но сахар все равно лучше)
3. Возможных реализаций несколько и одна из них — та, что в шарпе — конечный автомат. На build2013 (кажется, хотя может это был и going native) был доклад о реализации __async/__await в VS. Для каждой операции, выполняющейся асинхронно, динамически выделяется отдельный стек фрейм и указатель стека перекидывается между фреймами в прологе/эпилоге функции.
Почему-то думал, что будет говорится как раз о корутинах и yeld, а последний здесь упомянут, но не в контексте сопроцедур, а на них же тоже можно строить вполне удобную замену колбэкам.
К слову о плюшках в свежих плюсовых компиляторах: недавно попробовал сделать массив из функций с помощью списка инициализации в ICC. Вот результат. Я немного в шоке :)
Ну, насколько я понимаю, лямбда имеет другой тип, нежели функция с такой же сигнатурой. Но AV — это конечно, слишком.
Это ещё хорошо, что AV вылезло. А это ведь значит, что там совсем неопределённое поведение, оно могло и скомпилироваться, а вылезти уже потом в программе случайным образом.
Да, но в MSVC почему-то работает на ура.
Интересно, а кого они троллят, вводя в VS 2012 в диалекте C++/CLI такой синтаксис?
public ref class FooBase abstract : public BarBase {

..
		virtual void GetFoo(Platform::WriteOnlyArray<int>^ params);
internal:
		void Bar(Platform::Object^ sender) override
		{
				Window::Current->CoreWindow->SizeChanged += 
					ref new TypedEventHandler<CoreWindow^, WindowSizeChangedEventArgs^>(sender, &FooBase::OnWindowSizeChanged);
		}
..
};


C# программисты в шоке, С++ программисты морщатся в отвращении. Смысл, конечно, понятен, но мне жалко тех, кому прийдется такой «С++» код саппортить или даже портировать на другую платформу.
И мне жалко. Но вообще весь С++/CLI — это либо чтобы сделать тонкий клиент от С++ кода к дотнету (если из него чего вдруг надо), или тонкая прослойка между основным продуктом на .NET и проверенной библиотекой на С++, которую переписывать нет ни желания ни нужды.

В пределах пары сотен строк с С++/CLI ещё можно жить. Писать на нём целиком большой продукт — это надо быть хорошо двинутым.
Я на нем писал курсовые в 2007..2008 годах, когда препод требовал «обязательно на С++», но хотелось функционала .NET :)
В целом — терпимо, но на тот момент я еще не знал C#. После него смотреть назад на C++/CLI вообще невозможно.
Это C++/CX. Я только понять не могу чего C++ программисты морщатся? Делегаты сам приводить он не умеет, потому такая страшная строчка в Bar() вышла. В остальном все довольно неплохо.
Имхо вполне правильно тащить удачные фичи из одного языка в другой.

В C++ обязательно надо добавить async\await и yield из C#. А в C# надо реализовать шаблоны, как в C++ или хотябы подмножество, как в F#.

А еще в обоих надо TypeClasses как в Цацкелле.
А в C# надо реализовать шаблоны, как в C++

Странно. Я всегда считал, что в С++ самые ужасные шаблоны.
Они ужасны, но полны по Тьюрингу, поэтому на них можно реализовать хоть чёрта лысого.
Лучше постшарп прикрутить в таком случае
Полны, да не совсем, ибо компиляторы накладывают ограничения на глубину вычисления шаблонов. Так что чёрт лысый должен быть относительно просто устроен.
самые ужасные шаблоны
Очевидно, по сравнению со всеми остальными языками.
По сравнению с C#, например.
В C# нет шаблонов, есть Generic. Шаблоны инстанцируются в compile-time и могут, например, реализовать rank-2 полиморфизм.
А F# есть подобная штука, называется compile-time generics, но она работает только в inline функциях, что сильно ограничивает применение.
Я понимаю, что шаблоны и generics — это не одно и то же. Generics из C# красиво и элегантно вписан в систему типов, а шаблон в C++ — это тупо макрос, который тупо подставляет типы в нужные места, а потом пытается скомпилить.
Я понимаю, что макросы — это тоже хорошо. Местами, они бывают незаменимы. Но я бы предпочел язык с мощной системой типов, языку с мощной системой макросов.
А почему нельзя совместить? Что принципиально мешает сделать compile-time generics в C#? Что мешает сделать явные ограничения для шаблонов в C++?
Да ничто не мешает. Теоретически могло быть и то, и то.
Более того, concepts в C++ (и его новая инкарнация — concepts lite) именно об этом.

О, они сделали лайт-версию! Спасибо, не знал. Сразу было понятно, что предыдущая не взлетит.
Как минимум, generics в C# не поддерживает ни явную, ни частичную специализацию, ни переменное число параметров (variadic generics), а также не позволяет задавать типы в качестве параметров по-уолчанию и определять нешаблонные типы.
std::yield реализовать стоит — его реализация в духе недавних добавлений функциональщины. А зачем await? Какая-то нехорошая идея у [Microsoft?] плодить кучу отдельных слов (async хотя бы стоит в определённом месте и его можно не делать зарезервированных в других местах), хотя всё можно прекрасно запихнуть в методы std::futures.
И я так и не понял, зачем нужны асинхронные не системные вызовы? Они ж всё равно не смогут выполняться в то же время (а неявный вызов других потоков — это слишком высокоуровнево для включения в C++).
Кстати вопрос знатокам.
Если я правильно понимаю, то работать async\await будет ровно как в C# — переписывать функции в КА, а замыкания в классы. Как при этом будет сделан контроль времени жизни\владения? Где будет храниться объект-замыкание?
Это какой-то не С++-способ, оверхеда много. Скорее сделают выделение области данных в куче, туда поскладывают всё нужное, а контроль времени жизни этой области сделают каким-нибудь счетчиком ссылок на неё из всех замыканий. Но это так, фантазии. Посмотрим, как оно будет.
И что там с исключениями?

Ты, кстати, проходишь курс Мартина Одерского и Эрика Мейера «Principles of Reactive Programming» на курсере? В Scala те же async/await.
Я записался, но проходить времени нет. Может нагоню еще… (мечты-мечты)
Об этой теме неплохо рассказали на GoingNative
channel9.msdn.com/Events/GoingNative/2013/Bringing-await-to-Cpp
Из доклада становится ясно, что Microsoft включи эту фичу в компилятор по своим собственным соображениям, скорее всего ради пользователей WinRT.

Вообще какая-то странная статья.

std::future<int> f_int = make_dummy_future(42);
int i = f_int.get() // ждём окончания работы функции
f_int.then([](std::future<int> i){/* deal with it */})

Крайне сомнительное использование then. Зачем он, если результата мы уже синхронно дождались строчкой выше?

а вдруг у вас были переменные с именами __async\__await ?

ССЗБ, стандарт резервирует такие имена.
>Зачем он, если результата мы уже синхронно дождались строчкой выше?
Это два несвязанных друг с другом примера использования. Я согласен, что немного нелогично выглядит, но так в оригинале, а это всего лишь перевод.

>ССЗБ, стандарт резервирует такие имена.
Вон в предложении уже решили __async в resumable переименовать. Риск некоторый есть.
решили __async в resumable переименовать

Я где-то слышал (кажется, в Design and Evolution of C++), что новые ключевые слова, добавляемые в язык, выбираются странноватыми как раз для уменьшения вероятности коллизий. typename, к примеру странное слово, и обычный программист врядли решит так назвать класс или перменную. Думаю, с resumable аналогичная ситуация. Мне бы такое слово в голову само не пришло.
В предложении __async не было, было async.

Для правильного кода риска нет — по стандарту все имена, начинающиеся с двух подчеркиваний, зарезервированы для реализации языка (в т.ч. для языковых расширений). Именно отсюда все эти __async и __attribute__.
А потом комиссия по языку составит свой вариант данного решения и будет два несовместимых варианта реализации: MSVC и все остальные. Что-то мне это напоминает.
Херб Саттер (из Майкрософта по С++ который) — один из главных идеологов С++11, общих стандартов, большой друг Страуструпа и вообще мужик со здравым смыслом в голове. Вряд ли будет два несовместимых стандарта.
В оригинальной статье многое, увы, напутано. Причем это ровно те же грабли, на которые люди массово наступали, осваивая async/await в C#.

>> Внутри resumable функции вещи происходят тоже слегка иначе. Используя ключевое слово await теперь можно «завернуть» выражение или вызов функции в future, которая посчитает это выражение или вызов в другом потоке.

await ничего никуда не заворачивает — наоборот, она «разворачивает» значение типа std::future (возможно, но не обязательно, возвращенное из другой resumable функции), асинхронно ожидая завершения вычисления и вытаскивая полученное значение (или исключение).

Чтобы завернуть вызов обычной синхронной функции в future, нужно использовать std::async. Что, кстати, наглядно видно из приведенного там же примера кода.

>> И вот мы подоходим к интересному. Вы можете использовать ключевое слово await неоднократно — каждое его применение создаст std::future, которая начнёт выполняться в параллельном потоке.

Те же грабли, вид сбоку. Еще раз — await не создает экземпляры std::future, а получает значения из них.

Ну и о «параллельных потоках» — это тоже неправда. Далеко не каждый future выполняется на своем потоке. Например, вполне возможна реализация всяческой асинхронщины в reactor style, вообще на одном потоке (как в node.js) с циклом сообщений/коллбэков. Туда же всяческие completion ports и прочие средства асинхронного I/O.

Единственный случай, когда future гарантированно выполняется на своем потоке — это когда синхронный код обернут через std::async. Во всех остальных случаях, это деталь реализации данного конкретного future.
Небольшое замечание про «обновление компилятора — нетривиальное событие»
Вообще-то был «November 2012 CTP», (ну правда он был доступен уже в октябре ;-) )
В нем впервые появились многие фичи C++ 11.
Sign up to leave a comment.