[Перевод] Безопасная работа с памятью в D

Предисловие переводчика

Оригинальные статьи вышли с июня 2022-го по январь 2023-го в виде трёх постов на blog.dlang.org под общим заголовком «Безопасность памяти в современных системных языках программирования». Статьи посвящены DIP1000 — набору изменений, призванному существенно улучшить безопасность работы с памятью. Перевод объединяет все три.

Если стремитесь глубже разобраться с использованием @safe-кода, атрибутов scope и return scope и узнать про автовыведение атрибутов функции, эта статья может оказаться полезной.

В тексте «undefined behavior» иногда переведено как «неопределённое поведение», иногда просто записано в виде сокращения «UB». То же касается «garbage collector» — «сборщик мусора» или «GC».

Для экспериментов я использовал компиляторы dmd 2.102.1 и ldc 1.30.0. В них DIP1000 ещё не включён по умолчанию.

В целом данная статья ориентирована на D-программистов. Если вы мало знакомы с D, полезно будет узнать, что такое UFCS, т.к. это явление будет часто встречаться в примерах кода. Аббревиатура расшифровывается как «Uniform Function Call Syntax», т.е. «универсальный синтаксис вызова функций». Это особенность языка, позволяющая обычные функции (или шаблонные функции) вызывать так, как если бы они были методами — через точку после первого аргумента. К примеру, вызовы writeln(x) и x.writeln() означают одно и то же (а в последнем случае можно скобки и не писать).

Также я осмелился оставить некоторое количество примечаний, обозначенных как «примечание переводчика»; мелкие изменения в комментариях в коде оставил без пометок.

Часть 1

Безопасность работы с памятью не нуждается в проверках

D — это язык программирования, в котором присутствует как сборка мусора, так и эффективный прямой доступ к памяти. Современные языки высокого уровня, такие как D, безопасно работают с памятью, предотвращая нарушение системы типов языка и случайное чтение/запись в неиспользуемую память.

Как язык системного программирования, «не весь» в D даёт такие гарантии, но у него есть безопасное подмножество, в котором для управления памятью используется сборщик мусора, так же как в Java, C# или Go. Код на D, даже в проекте, предполагающем системное программирование, должен стремиться оставаться в рамках этого безопасного подмножества, где это целесообразно. В D имеется атрибут @safe для проверки того, что функция использует только безопасные для памяти возможности языка. К примеру, попробуйте такой пример:

@safe string getBeginning(immutable(char)* cString)
{
    return cString[0..3];
}

Компилятор откажется компилировать данный код, т.к. невозможно выяснить, что выйдет в результате получения трёхсимвольного среза из указателя cString, который может ссылаться на пустую строку (с нулевым символом в первом элементе), на строку с одним символом, или там может быть 1 или 2 символа, после которых нет нулевого. Тогда результатом будет нарушение работы с памятью. (Компилятор выводит ошибку «pointer slicing not allowed in safe functions». Если убрать @safe, поведение будет аналогичным таковому в C/C++, т.е. срез возьмёт больше байт, чем есть реально в строке под указателем cString. — Примечание переводчика.)

@safe — не значит медленный

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

И да, такие проекты время от времени вынуждены прибегать к небезопасным для памяти конструкциям. Даже у проектов более высокого уровня часто есть для этого причины, поскольку в них могут создаваться интерфейсы к Си-шным или C++-ным библиотекам. Также может быть желание отказаться от сборщика мусора, если к этому подводят показатели производительности. Но всё же довольно большие части кода могут быть помечены как @safe без использования сборщика мусора вообще.

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

@safe void add(int* a, int* b, int* sum)
{
    *sum = *a + *b;
}

Эта функция скомпилируется и она полностью безопасна для памяти, несмотря на разыменование указателей ровно тем же непроверенным способом, что и в C. Это безопасно для памяти, потому что @safe D не позволяет создавать указатели int*, указывающие на нераспределённые области памяти, или указывающие, например, на float**. Переменная int* может указывать на нулевой адрес, но это обычно не является проблемой безопасности памяти, потому что нулевой адрес защищён операционной системой. Любая попытка его разыменовать приведёт к падению программы ещё до того, как произойдёт повреждение памяти. (Вы увидите ошибку сегментирования. — Примечание переводчика). Сборщик мусора здесь не задействован, поскольку в D он работает когда у него запрашивается больше памяти или если сборщик вызван явно.

Срезы массивов так и работают: при индексации во время выполнения проверяется, что индекс меньше длины. Не будет никаких проверок на предмет того, ссылается ли срез на на легальную область памяти. Безопасность работы с памятью достигается путём предотвращения создания таких срезов. (Опять же, сборщик мусора здесь не участвует.)

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

struct Struct
{
    int[] slice;
    int* pointer;
    int[10] staticArray;
}

@safe @nogc Struct examples(Struct arg)
{
    arg.slice[5] = *arg.pointer;
    arg.staticArray[0..5] = arg.slice[5..10];
    arg.pointer = &arg.slice[8];
    return arg;
}

Как было продемонстрировано, D позволяет свободно выполнять непроверенные операции с памятью в @safe-коде. Память, на которую ссылаются arg.slice и arg.pointer, может находиться в куче, управляемой GC, или в статической памяти. Нет причин, по которым языку нужно об этом заботиться. Программе, вероятно, нужно будет вызвать сборщик мусора или выполнить какое-нибудь небезопасное выделение памяти, но обработка уже выделенной памяти не требуется. Если бы сама функция examples() нуждалась в сборщике мусора, она бы не скомпилировалась из-за атрибута @nogc.

Примечание переводчика.
Если в arg.slice нет элемента с индексом 5 (первое действие в фукнции) или 8 (третье действие в фукнции), будет кинуто исключение core.exception.ArrayIndexError. Также, если arg.slice не имеет элементов [5…10], в arg.staticArray не запишется ничего и будет кинуто исключение core.exception.ArraySliceError. Причём эти исключения будут кинуты вне зависимости от наличия атрибута @safe у функции examples().

Однако…

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

@safe @nogc Struct examples(Struct arg)
{
    arg.pointer = &arg.staticArray[8];
    arg.slice = arg.staticArray[0..8];
    return arg;
}

Структура здесь передана по значению, её содержимое скопировано в стек при вызове функции и может быть перезаписано после возвращения из функции. Поле arg.staticArray — тоже тип-значение, оно копируется целиком вместе со структурой (как если бы вместо него было бы просто десять переменных типа int). Когда мы возвращаем arg из функции, содержимое staticArray копируется в возвращаемое значение, но pointer и slice продолжают указывать на arg (оставшийся в функции), а не на возвращаемую копию!

Но у нас есть исправление, позволяющее писать код в @safe-функциях так же эффективно, как и раньше, включая ссылки на стек. Это исправление даже позволяет безопасно писать несколько вещей, ранее доступных только в @system-режиме (противоположность @safe). Это исправление — DIP1000. Это же исправление является причиной, по которой данный пример уже вызывает предупреждение «deprecated», если он скомпилирован с последней ночной сборкой dmd.

Родился первым, умер последним

DIP1000 — это набор усовершенствований в правилах языка, касающихся указателей, срезов и других ссылочных типов данных. Название расшифровывается как D Improvement Proposal №1000, именно на этом документе изначально основывались новые правила. Включить новые правила можно с помощью флага компилятора -preview=dip1000. Существующий код может потребовать некоторых изменений для работы с новыми правилами, поэтому эта опция не включена по умолчанию. В будущем этот режим будет использоваться по умолчанию, поэтому лучше включить его там, где это возможно, и работать над совместимостью кода с ним там, где это невозможно.

Базовая идея заключается в том, чтобы позволить людям ограничить время жизни ссылки (массива или указателя, например). Указатель на стек не опасен, если он существует не дольше, чем стековая переменная, на которую он указывает. Обычные ссылки продолжают существовать, но они могут ссылаться только на данные с неограниченным временем жизни, т.е. на память, предоставленную сборщиком мусора, или статические или глобальные переменные.

Давайте начнём

Самый простой способ создания ссылок с ограниченным временем жизни — присвоить ссылке что-то с ограниченным временем жизни.

@safe int* test(int arg1, int arg2)
{
    int* notScope = new int(5);
    int* thisIsScope = &arg1;
    int* alsoScope;
    alsoScope = thisIsScope;

    // Ошибка! Переменная, объявленная ранее, считается
    // имеющей большее время жизни, поэтому это присвоение запрещено
    thisIsScope = alsoScope;

    return notScope; // всё хорошо
    return thisIsScope; // ошибка
    return alsoScope; // ошибка
}

При проверке этих примеров не забывайте использовать флаг компилятора -preview=dip1000 и пометить функцию атрибутом @safe. Проверки не выполняются для функций, не являющихся @safe.

Примечание переводчика.
Без -preview=dip1000 пример тоже не скомпилируется, причём ошибка на этот раз проявится на строке int* thisIsScope = &arg1;. В сообщении об ошибке будет сказано, что нельзя брать адрес локальной переменной в @safe-функции. И здесь режим с DIP1000 действует явно гораздо интеллектуальнее.

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

@safe int[] test()
{
    int[] normalRef;
    scope int[] limitedRef;

    if(true)
    {
        int[5] stackData = [-1, -2, -3, -4, -5];

        // Время жизни stackData заканчивается раньше,
        // чем limitedRef, поэтому так делать нельзя:
        limitedRef = stackData[];

        // вот так можно:
        scope int[] evenMoreLimited = stackData[];
    }

    return normalRef; // всё хорошо
    return limitedRef; // запрещено
}

Если мы не можем возвращать из функций ссылки с ограниченным временем жизни, как мы можем ими пользоваться? Довольно просто. Помните, защищён только адрес на данные, а не сами данные. Это означает, что у нас есть много способов передать scoped-данные из функции.

@safe int[] fun()
{
    scope int[] dontReturnMe = [1, 2, 3];

    // выделяем память под массив обычным образом (с GC)
    int[] result = new int[](dontReturnMe.length);
    // Здесь происходит копирование данных,
    // чтобы результат не ссылался на защищённую память
    result[] = dontReturnMe[];
    return result;

    // сокращённый способ сдалать то же, что и выше
    return dontReturnMe.dup;

    // Вас не всегда может интересовать содержимое,
    // вы можете захотеть что-то вычислить
    return
    [
        dontReturnMe[0] * dontReturnMe[1],
        cast(int) dontReturnMe.length
    ];
}

Между функциями

DIP1000 мог бы ограничиться языковыми примитивами при работе с ссылками с ограниченным временем жизни (с помощью рассмотренных выше приёмов), но класс хранения scope можно применять и к параметрам функций. Это гарантирует, что память не будет использована после выхода из функции, а, значит, ссылки на локальные данные могут использоваться в качестве аргументов в scope-параметрах.

@safe double average(scope int[] data)
{
    double result = 0;
    foreach(el; data) result += el;
    return result / data.length;
}

@safe double use()
{
    int[10] data = [1,2,3,4,5,6,7,8,9,10];
    return data[].average; // работает!
}

Примечание переводчика.
Использование среза массива без индексов (data[]) — это, в данном случае, фактически, создание динамического массива из статического. Причём на самом деле, можно обойтись без квадратных скобок: массив неявно преобразуется в динамический массив при передаче в функцию average(). В любом случае data.ptr внутри функции average() будет тот же, что и в функции use(), а, значит, можно влиять на содержимое переданного массива. Какой в таком случае смысл использования scope в данном примере, не ясно. Как было замечено автором выше, «защищён только адрес на данные, а не сами данные». Иное дело, если параметр в average() будет const scope — тогда данные внутри функции точно не будут изменены.

Поначалу, вероятно, лучше воздержаться от автоматического выведения атрибутов. Автовыведение в целом — хороший инструмент, но он тихонько добавляет атрибуты scope ко всем параметрам, до которых достаёт, что означает, что можно легко потерять контроль, что значительно усложняет выяснение происходящего. Чтобы избежать этого, можно всегда явно указывать возвращаемый тип (или его отсутствие с void или noreturn):

@safe const(char[]) fun(int* val)
против
@safe auto fun(int* val)
или
@safe const fun(int* val)

Также функция не должна быть шаблоном или находиться внутри шаблона. Более подробно об автовыведении с атрибутом scope мы расскажем в одном из следующих постов.

Атрибут scope позволяет работать с указателями и массивами, указывающими на стек, но запрещает возвращать их. А что если нам всё же нужно их возвращать? Тогда можете использовать return scope:

// Будучи массивами символов, строки тоже работают с DIP1000
@safe string latterHalf(return scope string arg)
{
    return arg[$/2 .. $];
}

@safe string test()
{
    // размещено в статической памяти программы
    auto hello1 = "Hello world!";
    // выделено на стеке, скопировано из hello1
    immutable(char)[12] hello2 = hello1;

    auto result1 = hello1.latterHalf; // всё хорошо
    return result1; // всё хорошо

    auto result2 = hello2[].latterHalf; // всё хорошо
    // Хорошая попытка! Но result2 локален и не может быть возвращён
    return result2;
}

Примечание переводчика.
Здесь разница именно в месте размещения строки: hello2 является статическим массивом и ему нельзя выходить за пределы функции: при использовании -preview=dip1000 будет ошибка компиляции.
Если убрать return scope у параметра функции latterHalf(), можно будет увидеть сообщение о том, что ссылка на локальную переменную hello2 присваивается не-scope-параметру arg в вызове latterHalf(), чего делать с DIP1000 нельзя (ошибка компиляции). И да, речь всё ещё о @safe-функциях.

Параметры, помеченные как return scope, проверяют, является ли любой из переданных аргументов scope. Если да, то возвращаемое значение обрабатывается как scope-значение, которое не может пережить ни один из аргументов, помеченных как return scope. Если ни один аргумент не является scope, возвращаемое значение рассматривается как глобальная ссылка, которую можно свободно копировать. Как и scope, return scope консервативен. Даже если на самом деле возвращается не адрес, защищённый благодаря return scope, компилятор всё равно выполнит проверку времени жизни.

scope не идёт вглубь

@safe void test()
{
    scope a = "first";
    scope b = "second";
    string[] arr = [a, b];
}

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

Однако, подумайте, что scope в случае scope string[] сможет защитить? Потенциально это две вещи: адреса строк в массиве или адреса символов в строках. Чтобы это присваивание было безопасным, scope должен защищать символы в строках, но он защищает только ссылку верхнего уровня, т.е. строки в массиве. Таким образом, пример не работает. Теперь изменим arr так, чтобы это был статический массив:

@safe void test()
{
    scope a = "first";
    scope b = "second";
    string[2] arr = [a, b];
}

Это работает, потому что статические массивы не являются ссылочными типами. Память для всех их элементов выделяется в стеке, в отличие от динамических массивов, которые содержат ссылку на элементы, хранящиеся в другом месте. Когда статический массив объявлен как scope, его элементы рассматриваются как scope. Поскольку пример не станет компилироваться, если arr — не scope, из этого следует, что здесь scope, можно сказать, подразумевается.

Несколько практических советов

Давайте посмотрим правде в глаза, правила DIP1000 требуют времени для понимания, и многие предпочитают потратить это время на программирование чего-нибудь полезного. Первый и самый важный совет: избегайте не-@safe-кода как чумы, если возможно. Конечно, в этом совете нет ничего нового, но в случае с DIP1000 он становится ещё более важным. В двух словах, язык не проверяет валидность scope и return scope в не-@safe-функции, но при вызове этих функций компилятор предполагает, что предполагаемое обозначенными атрибутами соблюдено.

Это делает scope и return scope в небезопасном коде грозными ружьями, направленными себе в ногу. Если D-программист будет сопротивляться искушению пометить код как @trusted, он (программист) вряд ли сможет причинить вред. Неправильное использование DIP1000 в коде @safe может привести к избыточным ошибкам компиляции, но не приведёт к повреждению памяти и вряд ли вызовет другие проблемы.

Второй важный момент, о котором стоит упомянуть, заключается в том, что нет необходимости в использовании scope и return scope для атрибутов функции, если они получают только статические или выделенные сборщиком мусора данные. Многие языки вообще не позволяют программистам обращаться к стеку; то, что программисты на D могут это делать, не означает, что они должны это делать. Таким образом, им не придётся тратить больше времени на решение ошибок компилятора, чем до появления DIP1000. А если желание работать со стеком всё-таки возникнет, авторы смогут вернуться к аннотированию функций. Скорее всего, они сделают это, не ломая интерфейс.

Что дальше?

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

В любом случае, есть ещё много того, что стоит узнать. DIP1000 имеет некоторые возможности для ref-аргументов функций, для структур и объединений, которые мы здесь не рассмотрели. Мы также углубимся в то, как DIP1000 работает с не-@safe-функциями и автоопределением атрибутов. В настоящее время планируется ещё два поста этой серии.

Спасибо Уолтеру Брайту за рецензирование этой статьи.

Часть 2

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

Объекты классов — самый простой случай

В 1-й части я сказал, что, если вы понимаете, как DIP1000 работает с указателями, то вы понимаете, как он работает с классами. Перейдём сразу к примеру:

@safe Object ifNull(return scope Object a, return scope Object b)
{
    return a ? a : b;
}

В приведённом примере return scope работает точно так же, как и здесь:

@safe int* ifNull(return scope int* a, return scope int* b)
{
    return a ? a : b;
}

Принцип заключается в следующем: если scope или return scope применяется к объекту в списке параметров, адрес объекта класса защищается так же, как если бы параметр был указателем на объект. С точки зрения машинного кода, Object a — это указатель на объект.

Что касается обычных функций, то всё уже сказано. А что насчёт методов класса или интерфейса? Вот как это делается:

interface Talkative
{
    @safe const(char)[] saySomething() scope;
}

class Duck : Talkative
{
    char[8] favoriteWord;

    @safe const(char)[] saySomething() scope
    {
        import std.random : dice;

        // это не работает
        // return favoriteWord[];

        // а это работает
        return favoriteWord[].dup;

        // Также работает возврат чего-то совсем другого.
        // Здесь возвращается 1-я запись в 40% случаев,
        // 2-я — в 40% случаев и 3-я в остальных случаях.
        return
        [
            "quack!",
            "Quack!!",
            "QUAAACK!!!"
        ][dice(2,2,1)];
    }
}

Атрибут scope, расположенный либо до, либо после имени функции-члена, помечает ссылку this как scope, предотвращая её утечку из функции. Поскольку адрес объекта защищён, ничего, что ссылалось бы непосредственно на адреса полей, не допускается к выходу за пределы метода. Вот почему возвращение favoriteWord[] запрещено, это статический массив, хранящийся внутри экземпляра класса, поэтому возвращаемый фрагмент будет ссылаться непосредственно на него. С другой стороны, favoriteWord[].dup возвращает копию данных, которая не находится в экземпляре класса, поэтому это нормально.

В качестве альтернативы можно заменить атрибуты scope для Talkative.saySomething и Duck.saySomething на return scope, что позволит возвращать favoriteWord без дублирования.

DIP1000 и принцип подстановки Барбары Лисков

Принцип подстановки Барбары Лисков в упрощённом виде гласит, что унаследованная функция может дать вызывающей стороне больше гарантий, чем её родительская функция, но меньше — никогда. Атрибуты, связанные с DIP1000, попадают в эту категорию. Правило работает следующим образом:

  • если параметр (включая неявную ссылку this) в родительской функции не имеет атрибутов DIP1000, дочерняя функция может назначить ему scope или return scope;

  • если параметр обозначен как scope в родителе, в наследнике scope так же должен быть;

  • если параметр имеет атрибут return scope, он должен быть или return scope, или scope в наследнике.

Если атрибут отсутствует, вызывающая сторона не может ничего предполагать; функция может где-нибудь хранить адрес аргумента. Если присутствует return scope, вызывающая сторона может предположить, что адрес аргумента не хранится нигде, кроме как в возвращаемом значении. При наличии scope гарантируется, что адрес нигде не хранится, что является ещё более надёжной гарантией. Пример:

class C1
{
    double*[] incomeLog;

    // "impose tax" ­— "взимать налог", "income" — "доход"
    @safe double* imposeTax(double* pIncome)
    {
        incomeLog ~= pIncome;
        return new double(*pIncome * .15);
    }
}

class C2 : C1
{
    // Хорошо с точки зрения языка (но, возможно,
    // нечестно для налогоплательщика)
    override @safe double* imposeTax(return scope double* pIncome)
    {
        return pIncome;
    }
}

class C3 : C2
{
    // Тоже хорошо.
    override @safe double* imposeTax(scope double* pIncome)
    {
        return new double(*pIncome * .18);
    }
}

class C4 : C3
{
    // Нехорошо. Параметр pIncome в C3.imposeTax имеет атрибут scope,
    // а здесь мы пытаемся ослабить это ограничение.
    override @safe double* imposeTax(double* pIncome)
    {
        incomeLog ~= pIncome;
        return new double(*pIncome * .16);
    }
}

Особый указатель — ref

Мы всё ещё не выяснили, как использовать структуры и объединения в DIP1000. Что ж, мы разобрались с указателями и массивами. При обращении к структуре или объединению они работают так же, как и при обращении к любому другому типу. Но указатели и массивы не являются каноническим способом использования структур в D. Чаще всего они передаются по значению или по ссылке, когда связаны с ref-параметрами. Самое время объяснить, как работает ref в DIP1000.

Они работают не так, как иные указатели. Как только вы поймёте, что такое ref, вы сможете использовать DIP1000 так, как иначе не смогли бы.

Простой параметр ref int

Простейший возможный способ использовать ref, вероятно, следующий:

@safe void fun(ref int arg) {
    arg = 5;
}

Что это значит? Изнутри ref является указателем, считайте, что int* pArg, но в исходном коде используется как значение. Кроме того, клиент вызывает функцию, как если бы аргумент был передан по значению:

auto anArray = [1, 2];
fun(anArray[1]);  // с UFCS можно написать anArray[1].fun;
// anArray теперь содержит [1, 5]

При передаче по указателю мы бы писали fun(&anArray[1]). В отличие от ссылок в C++, ссылки в D могут быть нулевыми, но приложение мгновенно завершится с ошибкой сегментации, если нулевая ссылка будет использована для чего-то другого, кроме чтения адреса с помощью оператора &. Таким образом…

int* ptr = null;
fun(*ptr);

…этот код компилируется, но падает во время исполнения, потому что присваивание внутри fun приземляется прямо на нулевой адрес.

Адрес переменной ref всегда защищён от утечки. В этом смысле
@safe void fun(ref in arg) { arg = 5; }
подобен
@safe void fun(scope int* pArg) { *pArg = 5; }.
А, например,
@safe int* fun(ref int arg) { return &arg; }
не будет компилироваться, так же как и
@safe int* fun(scope int* pArg) { return pArg; }

Однако, существует класс хранения return ref, который позволяет возвращать адрес параметра. Это означает, что
@safe int* fun(return ref int arg) { return &arg; }
работает.

Ссылка на ссылку

Ссылка ref int или аналогичный тип уже позволяет использовать гораздо более красивый синтаксис, чем при работе с указателями. Но настоящая сила ref проявляется, когда он ссылается на тип, который сам является ссылкой, например, указатель или класс. Атрибуты scope и return scope могут быть применены к ссылке, на которую ссылается ref. Пример:

@safe float[] mergeSort(ref return scope float[] arr)
{
    import std.algorithm : merge;
    import std.array : Appender;

    if (arr.length < 2) return arr;

    auto half1 = arr[0 .. $/2];
    auto half2 = arr[$/2 .. $];

    Appender!(float[]) output;
    output.reserve(arr.length);

    foreach (el; half1.mergeSort.merge!floatLess(half2.mergeSort)) {
        output ~= el;
    }

    arr = output[];
    return arr;
}

@safe bool floatLess(float a, float b)
{
    import std.math : isNaN;
    return a.isNaN ? false : b.isNaN ? true : a < b;
}

Здесь mergeSort гарантирует, что не будет передавать адрес чисел в arr, кроме как в возвращаемом значении. Это та же гарантия, что и при возврате параметра float[] arr из параметра return scope float[] arr. Но, в то же время, поскольку arr является ref-параметром, mergeSort может изменять переданный ему массив. Тогда в клиентском коде можно написать:

float[] values = [5, 1.5, 0, 19, 1.5, 1];
values.mergeSort;

С не-ref-аргументом клиенту пришлось бы написать values = values.sort вместо этого (отказ от использования ссылки был бы вполне разумным решением API в данном случае, поскольку мы не всегда хотим изменять исходный массив). Это то, чего нельзя достичь с помощью указателей, поскольку return scope float[]* arr будет защищать адрес метаданных массива (поля length и ptr массива), а не адрес его содержимого.

@safe ref Exception nullify(return ref scope Exception obj)
{
    obj = null;
    return obj;
}

@safe unittest
{
    scope obj = new Exception("Error!");
    assert(obj.msg == "Error!");
    obj.nullify;
    assert(obj is null);
    // Поскольку nullify возвращает результат по ссылке,
    // мы можем новое значение присвоить результату сразу на месте
    obj.nullify = new Exception("Fail!");
    assert(obj.msg == "Fail!");
}

Здесь мы возвращаем адрес аргумента, переданного для nullify, но при этом защищаем адрес указателя на объект и адрес экземпляра класса от утечки по другим каналам.

Ключевое слово return не требует, чтобы за ним следовали ref или scope. Что тогда означает void* fun(ref scope return int*)? В спецификации говорится, что return без последующего scope всегда рассматривается как ref return. Таким образом, данный пример эквивалентен void* fun(return ref scope int*). Однако это применимо только в том случае, если есть ссылка для привязки. Запись void* fun(scope return int*) означает void* fun(return scope int*). Можно даже написать void* fun(return int*) с последним упомянутым смыслом, но я оставляю на ваше усмотрение, будет ли это считаться краткостью или запутыванием.

Функции-члены и ref

Атрибуты ref и return ref часто требуют внимательного вглядывания, чтобы оследить, какой адрес защищён и что может быть возвращено. Требуется определённый опыт, чтобы освоиться с ними. Но как только вы это сделаете, понимание того, как работают структуры и объединения в DIP1000, станет довольно простым. Основное отличие от случая с классами заключается в том, что, если в функциях-членах класса ссылка this является обычной ссылкой на объект класса, то в функциях-членах структуры или объединения ссылка this — это ref StructOrUnionName.

union Uni
{
    int asInt;
    char[4] asCharArr;

    // возвращаемое значение содержит ссылку на содержимое
    // этого объединения, но ссылки на него не будут
    // утекать каким-либо другим путём
    @safe char[] latterHalf() return
    {
        return asCharArr[2 .. $];  // или this.asCharArr[2 .. $];
    }

    // this-аргумент является неявным ref, а написанное ниже означает,
    // что возвращаемое значение не ссылается на это объединение,
    // а также то, что мы не "сливаем" его каким-либо другим способом
    @safe char[] latterHalfCopy()
    {
        return latterHalf.dup;  // или this.latterHalf.dup;
    }
}

Обратите внимание, что return ref не должен использоваться с аргументом this. Выражение char[] latterHalf() return ref компилятор не сможет распарсить. Язык уже должен понимать, что ref char[] latterHalf() return означает, что возвращаемое значение является ссылкой. В любом случае ref в return ref будет лишним.

Заметьте ещё, что мы не использовали здесь ключевое слово scope. Атрибут scope был бы бессмысленным для этого объединения, потому что оно не содержит ссылок на что бы то ни было. Точно так же бессмысленно иметь атрибуты scope ref int или scope int у аргумента функции, ведь scope имеет смысл только для типов, которые ссылаются на память в другом месте.

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

struct CString
{
    // Нам нужно поместить указатель в анонимное объединение с
    // фиктивным членом, иначе клиентский @safe-код сможет присвоить
    // указателю ptr ссылку на символ, не входящий в Си-строку.
    union
    {
        // Пустые строковые литералы оптимизируются компилятором D
        // в нулевые указатели, поэтому мы делаем так, чтобы указатель
        // действительно указывал на '\0'; nullChar объявлен ниже по коду
        immutable(char)* ptr = &nullChar;
        size_t dummy;
    }

    // return scope здесь гарантирует, что эта структура
    // не будет жить дольше, чем память в arr.
    @trusted this(return scope string arr)
    {
        // Примечание: обычные assert не подойдут! Они могут быть удалены
        // из релизных сборок, но этот assert необходим для безопасности
        // памяти, поэтому нам нужно использовать assert(0), который
        // никогда не будет удалён.
        if (arr[$-1] != '\0') assert(0, "not a C string!");
        ptr = arr.ptr;
    }

    // Возвращаемое значение ссылается на ту же память, что и члены
    // этой структуры, но мы не "сливаем" ссылки на неё каким-либо
    // другим способом, поэтому используем return scope.
    @trusted ref immutable(char) front() return scope
    {
        return *ptr;
    }

    // Ссылки на указанный массив нигде не передаются.
    @trusted void popFront() scope
    {
        // В противном случае пользователь сможет
        // проскочить конец строки и затем прочитать её!
        if (empty) assert(0, "out of bounds!");
        ptr++;
    }

    // То же самое.
    @safe bool empty() scope
    {
        return front == '\0';
    }
}

immutable nullChar = '\0';

@safe unittest
{
    import std.array : staticArray;

    auto localStr = "hello world!\0".staticArray;
    auto localCStr = localStr.CString;
    assert(localCStr.front == 'h');

    static immutable(char)* staticPtr;

    // Ошибка: утечка ссылки на локальное значение.
    // staticPtr = &localCStr.front();

    // Здесь всё хорошо.
    staticPtr = &CString("global\0").front();

    localCStr.popFront;
    assert(localCStr.front == 'e');
    assert(!localCStr.empty);
}

В первой части говорилось о том, что @trusted — это опасное ружье для выстрелов в ногу с DIP1000. Этот пример демонстрирует, почему. Представьте, как легко было бы ошибиться, использов обычный assert или вообще забыть об assert, или упустить из виду необходимость использования анонимного объединения. Я думаю, что эта структура безопасна для использования, но вполне может быть, что я что-то упустил из виду.

И под конец

Мы почти всё уже знаем об использовании структур, объединений и классов с DIP1000. Сегодня нам осталось узнать две последние вещи.

Но перед этим небольшое отступление о ключевом слове scope. Оно используется не только для аннотирования параметров и локальных переменных, как было проиллюстрировано. Оно также используется для scope-классов и для стражей области видимости. В данном руководстве они обсуждаться не будут, поскольку первая функция устарела, а вторая не имеет отношения к DIP1000 или контролю времени жизни переменных. Смысл упоминания о них в том, чтобы развеять потенциальное заблуждение, что scope всегда означает ограничение времени жизни чего-либо. Изучение операторов стражей области видимости по-прежнему является хорошей идеей, поскольку это полезная функция.

Вернёмся к теме. Первая вещь не совсем специфична для структур или классов. Мы обсудили, что обычно означают return, return ref и return scope, но у них есть и другой смысл. Глянем:

@safe void getFirstSpace
(
    ref scope string result, return scope string where
)
{
    //...
}

Обычное значение атрибута return здесь не имеет смысла, поскольку функция имеет тип возврата void. В этом случае действует специальное правило: если тип возврата void, а первый аргумент — ref или out, то любой последующий return ref/scope предполагается утекающим присвоением первому аргументу. В случае функций-членов структур предполагается, что они присваиваются самой структуре.

@safe unittest
{
    static string output;
    immutable(char)[8] input = "on stack";
    // Попытка присвоить статической переменной содержимое стека.
    // Не скомпилируется.
    getFirstSpace(output, input);
}

Поскольку речь зашла об атрибуте out, следует сказать, что здесь он будет лучшим выбором для результата, чем ref. Атрибут out работает как ref с той лишь разницей, что данные, на которые ссылается параметр out, автоматически инициализируются по умолчанию в начале функции, т.е. любые данные, на которые ссылается out-параметр, гарантированно не повлияют на выполнение функции.

Второе, что необходимо усвоить, — это то, что scope используется компилятором для оптимизации размещения классов внутри тел функций. Если новый класс используется для инициализации scope-переменной, компилятор может поместить её в стек

© Habrahabr.ru