LINUX.ORG.RU

Rust 1.13

 


4

10

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

Этот сезон оказался очень плодотворным для Rust. Проведены конференции RustConf, RustFest и Rust Belt Rust. Обсуждено будущее языка, разработан план на 2017 год и созданы новые инструменты.

Новое в 1.13

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

Cargo в этом релизе содержит важные обновления безопасности, связанные с зависимостями от curl и OpenSSL, для которых также недавно были опубликованы обновления безопасности. Подробную информацию можно найти в соответствующих источниках для curl 7.51.0 и OpenSSL 1.0.2j.

Оператор ?

Добавлен новый оператор ?, делающий обработку ошибок приятнее за счёт уменьшения визуального шума. В качестве иллюстрации представим следующий код, который считывает некоторые данные из файла:

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("username.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

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

С оператором ?, вышестоящий код выглядит следующим образом:

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("username.txt")?;
    let mut s = String::new();

    f.read_to_string(&mut s)?;

    Ok(s)
}

Оператор ? заменяет весь код обработки ошибок, написанный при помощи оператора match ранее. Иными словами, ? применяется к значению Result, и если оно равно Ok, разворачивает его и отдаёт вложенное значение; если это Err, то происходит возврат из функции, в которой вы находитесь.

Более опытные пользователи могут заметить, что этот оператор делает то же самое, что и макрос try!, который доступен начиная с Rust 1.0. И будут правы, в самом деле, это то же самое. До 1.13 read_username_from_file можно было бы написать следующим образом:

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = try!(File::open("username.txt"));
    let mut s = String::new();

    try!(f.read_to_string(&mut s));

    Ok(s)
}

Так зачем надо было расширять язык, если до этого уже был такой макрос? Есть несколько причин. Во-первых, try! доказал своё огромное значение и часто используется в идеоматичном Rust. Он используется так часто, что было принято решение о создании собственного «подслащенного» синтаксиса для него. Такой вид эволюции — одно из преимуществ мощной системы макросов: расширения к синтаксису языка можно добавлять через прототипирование без внесения изменений в сам язык и особо полезные макросы могут указать на недостающие возможности языка. Эволюция try! в ? — яркий пример этого.

Другая причина — восприятие нескольких последовательных вызовов try!:

try!(try!(try!(foo()).bar()).baz())
против
foo()?.bar()?.baz()?

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

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

Более подробно об операторе ? можно прочитать в RFC 243.

Улучшение производительности

В последнее время очень много внимания заострено на производительности компилятора. Mark Simulacrum и Nick Cameron произвели улучшения http://perf.rust-lang.org/, инструмента для отслеживания производительности компилятора, на котором периодически запускается набор rustc-benchmarks на выделенном оборудовании. Инструмент записывает результаты каждого прохода компилятора и позволяет находить и отслеживать код, приведший к регрессии. Например, при помощи этого инструмента можно посмотреть график производительности за весь цикл разработки релиза 1.13, где можно увидеть заметное сокращение времени работы компилятора, отдельно представленное на соответствующей странице со статистикой.

Большое улучшение на графике от 1 сентября связано с оптимизацией от Niko по кешированию нормализованных проекций во время преобразования. То есть во время генерации промежуточного представления LLVM компилятор больше не пересчитывает каждый раз конкретные экземпляры связанных типов, когда они необходимы, а использует ранее вычисленные значения. Несмотря на то, что данная оптимизация не влияет на всю кодовую базу, для некоторого кода с определенным шаблоном, например, futures-rs, ускорение сборки в режиме отладки достигает 40%.

Другая оптимизация от Michael Woerister уменьшает время компиляции библиотек, экспортирующих множество встраиваемых функций. Когда функция помечена как «#[inline]», в дополнение к преобразованию этой функции в текущей библиотеке компилятор сохраняет её представление MIR и преобразует функцию в представление LLVM в каждой библиотеке, которая вызывает её. Оптимизация, сделанная Michael Woerister, позволяет компилятору избегать предварительных преобразований кода встраиваемых функций в библиотеках, в которых они определены, до их непосредственного прямого вызова. Таким образом, компилятор избавляется от необходимости выполнения лишних шагов по преобразованию функции в промежуточное представление LLVM, оптимизации LLVM и преобразования функции в машинный код.

В некоторых случаях это приводит к впечатляющим результатам. Например, время сборки библиотеки ndarray уменьшилось на 50%, а библиотека winapi 0.3 (ещё не опубликована) полностью избавилась от шага генерации машинного кода.

Но это ещё не всё: Nick Nethercote обратил своё внимание на производительность компилятора, сконцентрировавшись на профилировании и микрооптимизациях. Этот релиз включает в себя некоторые плоды его работ, ещё больше ожидается в 1.14.

Другие заметные изменения

Макросы теперь можно использовать на позиции типов (RFC 873), а атрибуты могут быть применены к операторам (RFC 16):

// Use a macro to name a type
macro_rules! Tuple {
    { $A:ty,$B:ty } => { ($A, $B) }
}

let x: Tuple!(i32, i32) = (1, 2);

// Apply a lint attribute to a single statement
#[allow(uppercase_variable)]
let BAD_STYLE = List::new();

Были удалены встраиваемые флаги сброса. Раньше при условном перемещении компилятор встраивал «флаг сброса» в структуру (увеличивая его размер), чтобы отслеживать, когда надо его сбросить. Из-за этого некоторые структуры занимали больше места, что мешало передаче типов с деструкторами поверх FFI. Благодаря тому, что в версии 1.12 добавлен MIR, появилась основа для многих улучшений, включая удаление встраиваемых флагов сброса. Теперь флаги сброса хранятся в дополнительном слоте в стеке тех функций, которым они нужны.

Релиз 1.13 содержит серьёзную ошибку в генерации кода для ARM с аппаратной реализацией чисел с плавающей точкой. Поскольку 1.13 содержит исправление безопасности, пользователям ARM рекомендуется использовать бета-версии 1.14, в которых скоро появится исправление для ARM.

Стабилизация языка

Стабилизация библиотек

Возможности Cargo

Более детальный список изменений доступен по ссылке: https://github.com/rust-lang/rust/blob/stable/RELEASES.md#version-1130-2016-1...

>>> Подробности

★★★★★

Проверено: maxcom ()
Последнее исправление: cetjs2 (всего исправлений: 5)
Ответ на: комментарий от mersinvald

Интерфейсы браузеров уже давным давно пишут на html+js+css и рисуют на движке.

Разве? Firefox на XUL, Chrome тоже на своём движке. Сомневаюсь что табы, диалоги и менюшки рисуют на HTML.

RazrFalcon ★★★★★
()
Ответ на: комментарий от tailgunner

Еще раз - чего тебе не хватает? Или не устраивает, что PowerPC - всего лишь Tier 2?

А теперь сам внимательно посмотри на ссылочку которую прислал и скажи каким таким фигом ты будешь компилировать растокод если rustc отсутствует под платформы Tier2 чуть более чем полностью? Из 22 платформ rutc есть под 6 и 2 из них это x86 которые и так есть в Tier1.

И таки читай то на что ссылаешься перед тем как произвести вид.

Q-Master
()
Ответ на: комментарий от RazrFalcon

Ну ты напиши пример того-же что привел на Расте и будем сравнивать. Причем то что ты показал это C++-14 с лямбдами, которые пока еще мало используются как-раз из-за невнятного синтаксиса.

Вангую что то что будет написано на расте будет вообще нечитаемо.

Q-Master
()
Ответ на: комментарий от Q-Master

Еще раз - чего тебе не хватает? Или не устраивает, что PowerPC - всего лишь Tier 2?

А теперь сам внимательно посмотри на ссылочку которую прислал и скажи каким таким фигом ты будешь компилировать растокод если rustc отсутствует под платформы Tier2 чуть более чем полностью?

Tier 2 platforms can be thought of as «guaranteed to build»

И таки читай то на что ссылаешься перед тем как произвести вид.

Тебе прямо и немного направо.

tailgunner ★★★★★
()
Ответ на: комментарий от Q-Master

В том то и дело, что чтобы реализовать лайфтаймы - нужны закорючки. Чтобы реализовать шаблоны - нужны закорючки. Но раст плохой, а С++ нет. Логика/0.

Вопрос не в том, как это реализовать, а в синтаксисе.

RazrFalcon ★★★★★
()
Ответ на: комментарий от RazrFalcon

В Си тоже нет. Но это не мешает GTK+.

GTK+ мешает, gtk - нет.

И да, я читал и пытался писать на gtk. Адъ и Израиль. Что-то вроде vector_of_ints this_very_best_method_to_set_X_to_Y_by_using_setter_from_Z();

Через полчаса начинает болеть мозг.

Q-Master
()
Ответ на: комментарий от RazrFalcon

Вопрос не в том, как это реализовать, а в синтаксисе.

Ну так и приведи-же копию того что ты привел как пример для С++. Сразу станет понятно что читаемо и где меньше закорючек.

Q-Master
()
Ответ на: комментарий от tailgunner

Tier 2 platforms can be thought of as «guaranteed to build»

Генератор кода по шаблонам протокола для телеграм тоже guaranteed to build, но нифига не guaranteed to work.

Q-Master
()
Ответ на: комментарий от Q-Master

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

macro_rules! logical_and {
    ($($x: expr), *) => (true $(&& $x)*)
}
let add = |x: isize, y: isize| {
    let l = move || x;
    let r = move || y;
    move || l() + r()
};

Лямбды выглядят читабельней. Макра выглядит хуже, но у неё и возможностей больше.

Причем то что ты показал это C++-14 с лямбдами, которые пока еще мало используются как-раз из-за невнятного синтаксиса.

Датычо. А я знаю людей, которые в основном из-за них на C++11 переходили.

Esper
()
Ответ на: комментарий от eao197

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

Плюсовый синтаксис слишком длинен для беглого взгляда. Да, синтаксис раста шумный, да, каждый символ имеет значение, но всё можно прочесть и понять с одного взгляда, в отличие от func firsts<borrow A, borrow B>(x: borrow<A> Vector<int32>, y: borrow<B> Vector<int32>) -> tuple<borrow<A> int32, borrow<B> int32>.

quantum-troll ★★★★★
()
Последнее исправление: quantum-troll (всего исправлений: 1)
Ответ на: комментарий от Q-Master

Генератор кода по шаблонам протокола для телеграм тоже guaranteed to build, но нифига не guaranteed to work.

Не знаю, причем тут телеграм, но бинари для Tier 2 собираются и предоставляются. Если это «отсутствует чуть более, чем полностью» - ну окей. Я-то думал, что тебе не устраивает, что нет гарантированного запуска тестов.

tailgunner ★★★★★
()
Ответ на: комментарий от quantum-troll

Да, синтаксис шумный, да, каждый символ имеет значение, но всё можно прочесть и понять с одного взгляда

Видимо, для этого нужна сноровка. У меня не получилось.

в отличие от

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

func firsts(x: Vector<int32> in A, y: Vector<int32> in B) -> tuple<int32 in A, int32 in B>

И, наверняка, можно сделать и еще лучше.

eao197 ★★★★★
()
Последнее исправление: eao197 (всего исправлений: 1)
Ответ на: комментарий от quantum-troll

Плюсовый синтаксис слишком длинен для беглого взгляда.

Дело даже не в длине, а в том, что программисту ничего не даёт чтение лайфтаймов (кроме 'static). Они нужны только чтобы убедить компилятор в своей чистоплотности, поэтому их синтаксис лучше сделать максимально лаконичным. В приведённом коде они отвлекают на себя внимание от главного, и через это читать его решительно невозможно.

Esper
()
Ответ на: комментарий от eao197

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

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

Esper
()
Ответ на: комментарий от eao197

Потому, что:

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

static FOO: (i32,i32) = (3, 4);

fn foo<'a,'b>(x: &'a [i32], y: &'b [i32]) -> (&'a i32, &'b i32) {
    (&FOO.0, &FOO.1)
}

quantum-troll ★★★★★
()
Ответ на: комментарий от Esper

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

Тут выше один упоротый растоман устал бить себя пяткой в грудь утверждая, что у Rust-а C++ный синтаксис, а в C++ круглые скобки используются очень часто и вовсе не для обозначения типов. Кому из вас верить? ;)

Ну и лично для меня есть некая логическая непоследовательность в том, что HashMap от K и V записывается как HashMap<K,V>, а тупл из K и V, как (K,V), вместо Tuple<K,V>.

eao197 ★★★★★
()
Ответ на: комментарий от quantum-troll

1. Для лайфтаймов используются те же механизмы полиморфизма, что и для типов

Не понял.

2. Лайфтайм не означает принадлежность. К примеру, можно сделать вот так:

Тут вы, очевидно, не поняли. Запись x: &'a означает, что значение x будет отдолжено из лайфтайма с именем a. Это можно записать через любимый растоманами апостроф, а можно и вот так x: [i32] in A. И код будет выглядеть практически так же:

static FOO: (i32,i32) = (3,4);

func foo(x: [i32] in A, y: [i32] in B) -> (i32 in A, i32 in B) {
  (&FOO.0, &FOO.1)
}

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Не понял.

<'a> следует понимать буквально: функция (или тип), полиморфная относительно лайфтайма. Точно так же, как <T> означает полиморфизм относительно T.

Это можно записать через любимый растоманами апостроф, а можно и вот так x: [i32] in A.

Хорошо, тогда синтаксис ссылок без указания лайфтайма как будет выглядеть?

quantum-troll ★★★★★
()
Ответ на: комментарий от quantum-troll

<'a> следует понимать буквально: функция (или тип), полиморфная относительно лайфтайма. Точно так же, как <T> означает полиморфизм относительно T.

Ну а зачем список лайфтаймов перечислять отдельно? Если, например, в описании прототипа функции указание лайфтаймов может быть только после ключевого слова in, то можно просто писать что-то вроде:

func foo<K,V>(x: K in A, y: V in B) -> ...;
func bar<K,V>(x: K in static, y: V in A) -> ...;
Или я опять что-то не так понимаю.

Хорошо, тогда синтаксис ссылок без указания лайфтайма как будет выглядеть?

Просто без in LifetimeName.

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

func foo<K,V>( K &x in A, V &y in B ) -> ...
или, если растоманам не нравится C++ная запись «тип затем имя аргумента»:
func foo<K,V>( &x: K in A, &y: V in B ) -> ...

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Тут выше один упоротый растоман устал бить себя пяткой в грудь утверждая, что у Rust-а C++ный синтаксис

Это потому, что там имеют аналогичное значение угловые, фигурные скобки, :: и почти все операторы. Что можно было бескостыльно взять из C++ - взяли из C++.

в C++ круглые скобки используются очень часто и вовсе не для обозначения типов

В Расте они используются для того же, просто иногда ещё и для кортежей.

Ну и лично для меня есть некая логическая непоследовательность в том, что HashMap от K и V записывается как HashMap<K,V>, а тупл из K и V, как (K,V), вместо Tuple<K,V>.

Это просто тип, который удобно было сделать встроенным. Тебя же не удивляет, что в C++ невладеющие указатели имеют тип T*, а не pointer<T>?

Esper
()
Ответ на: комментарий от eao197

Ну а зачем список лайфтаймов перечислять отдельно?

А зачем список типов перечисляют отдельно? Может, <K, V> тоже не нужно? Да, кстати, у тебя синтаксис лайфтаймов неотличим от синтаксиса типов.

func foo<K,V>( &x: K in A, &y: V in B )

&x: K in A должно означать нечто противоположное x: &K in A. Но вообще, я как раз собирался написать в этом направлении:

Можно было использовать что-то вроде &T in 'a, но что тогда делать с &'a &T vs &&'a T?

quantum-troll ★★★★★
()
Ответ на: комментарий от Esper

Тебя же не удивляет, что в C++ невладеющие указатели имеют тип T*, а не pointer<T>?

Меня это не удивляет, т.к. я знаю, откуда есть пошел C++ и сколько всего доброго для него это принесло. Так что если бы в C++ указатели изначально обозначались как pointer<T> и owner<T>, то я был бы больше доволен.

eao197 ★★★★★
()
Ответ на: комментарий от Q-Master

Вы понимаете бессмысленность данного требования?

Приведите пример:

vec.iter().filter(|n| n > 0).map(|n| n * 2).collect()
на плюсах.

Или:

match n {
    0...99 => println("1"),
    100...199 => println("2"),
    200...299 => println("3"),
    _ => println("4"),
}

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

RazrFalcon ★★★★★
()
Ответ на: комментарий от quantum-troll

Да, синтаксис раста шумный, да, каждый символ имеет значение, но всё можно прочесть и понять с одного взгляда

Код на брейнфаке тоже можно прочесть и понять, компактность вообще улет. И по-китайски тоже можно читать, китайцы говорят что легко. А кто не может в китайский - быдло.

anonymous
()
Ответ на: комментарий от quantum-troll

Может, <K, V> тоже не нужно?

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

template<typename A, typename B, typename C>
auto foo(B x, C y) {...} // Типа A нет в списке аргументов.

foo<int>(0, 0.0);
Не знаю, есть ли в Rust-е подобное.

Можно было использовать что-то вроде &T in 'a, но что тогда делать с &'a &T vs &&'a T?

Не могу ответить, т.к. не знаю, чем отличается &'a &T от &&'a T.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

что у Rust-а C++ный синтаксис

Опять всё переврали. Я утверждал, что он довольно сильно совпадает, но не говорил что он идентичен.

Ну и лично для меня есть некая логическая непоследовательность в том

Фишка в том, что tuple в rust - это часть языка, а не часть std. В плюсах не смогли в язык впихнуть, впихнули в std. В том же питоне и map часть языка (ака словарь).

А в rust так сделали потому, что это не просто тип. У нас есть tuple struct и tuple enum, которые вашим способ не сделать.

Пример:

struct MyStruct(i32);

enum E {
    Color(u8, u8, u8),
}

RazrFalcon ★★★★★
()
Ответ на: комментарий от RazrFalcon

Не... Так не выйдет. В данной нише никого больше и нету.

Что, язык в этой нише обязан быть вырвиглазным винегретом?

anonymous
()
Ответ на: комментарий от khrundel

Делегирование должно быть явным.

А я что предлагаю? Первый пришедший в голову вариант:

impl T for S use S.a {
    fn qux(&self) { /* не делегируем, новая реализация */ }
}
Чем это менее явно, чем то, что необходимо писать сейчас?
impl T for S {
    fn foo(&self) { self.a.foo() }
    fn bar(&self) { self.a.bar() }
    fn baz(&self) { self.a.baz() }
    ...
    fn qux(&self) { /* не делегируем, новая реализация */ }
}
Аргумент, что во втором случае наоборот легче не заметить новую реализацию надо повторять?..

DarkEld3r ★★★★★
()
Ответ на: комментарий от RazrFalcon

В плюсах не смогли в язык впихнуть, впихнули в std.

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

anonymous
()
Ответ на: комментарий от eao197

Лайфтайм может использовать и в теле функции, для этого и перечисляется. Воспринимайте 'a как T. То есть это банальный параметризируемый тип.

RazrFalcon ★★★★★
()
Ответ на: комментарий от RazrFalcon

Я утверждал, что он довольно сильно совпадает, но не говорил что он идентичен.

Вы утверждали, что «чуть меньше чем полностью» совпадает. В переводе на нормальный язык это означает практически полную идентичность.

Фишка в том, что tuple в rust - это часть языка
У нас есть tuple struct и tuple enum

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

eao197 ★★★★★
()
Ответ на: комментарий от anonymous

Но это не помешало им добавить кучу других вещей.

RazrFalcon ★★★★★
()
Ответ на: комментарий от RazrFalcon

Практика показывает - да.

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

anonymous
()
Ответ на: комментарий от eao197

В переводе на нормальный язык это означает практически полную идентичность.

А это не так? Если не считать крайних случаев - код почти идентичен:

int i = 0;
if (i == 0) {
    printf("%i", i);
}
let i = 0;
if i == 0 {
    println!("{}", i);
}

for (int i : { 1, 2, 3 }) {
    printf("%i", i);
}
for i in &[ 1, 2, 3 ] {
    println!("{}", i);
}

А теперь напишите эти примеры на упомянутых perl и ocaml. Отличия будут разительные. Но да, проще придираться к словам.

RazrFalcon ★★★★★
()
Ответ на: комментарий от eao197

Чтобы даже над смыслом круглых скобочек приходилось подумать.

Как сложны быть вами.

RazrFalcon ★★★★★
()
Ответ на: комментарий от RazrFalcon

Не считая Ada (с натяжкой) - они все мертвы.

Скорее, вам просто не приходилось сталкиваться с теми нишами, в которых Pascal и его наследники используются.

Но если вам мало, добавьте сюда еще и Objective-C, и Swift.

eao197 ★★★★★
()
Ответ на: комментарий от RazrFalcon

А это не так?

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

let i = 0;
if i == 0 {
    println!("{}", i);
}
auto i = 0;
if( i == 0 )
  cout << i << endl;
Прям близнецы, блин, братья.

eao197 ★★★★★
()
Последнее исправление: eao197 (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.