Логическая константа в D

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

Логическая константа — это когда функция помечена как const, но разрешает доступ для записи к одной или нескольким переменным-членам. Типичное использование этого для ленивой оценки, например. (в С++)

struct Matrix
{
  double determinant() const
  {
    if ( m_dirty )
    {
      m_determinant = /* expensive calculation */;
      m_dirty = false;
    }
    return m_determinant;
  }

  void set(int i, int j, double x) { m_dirty = true; ...; }

  mutable bool m_dirty;
  mutable double m_determinant;
};

Здесь determinant() — это const, но он все еще может изменять m_dirty и m_determinant, поскольку они помечены как mutable.

В D const(FAQ) говорится, что D2 не поддерживает логическую константу. из-за слабой гарантии, которую он предоставляет, что мешает написанию параллельных программ и затрудняет некоторые оптимизации.

Я полностью понимаю проблему, но что, если нам нужна логическая константа?

Рассмотрим случай выше с классом Matrix, но без кэширования (и необходимости в логической константе). Также представьте, что этот класс используется во всей моей кодовой базе, и в основном доступ к нему осуществляется через константные ссылки.

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

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

Какие варианты у меня есть (если есть)?


person Peter Alexander    schedule 18.11.2010    source источник
comment
Если дорогостоящие вычисления можно выразить в виде чистой функции, то компилятор может выполнить кэширование за вас.   -  person he_the_great    schedule 19.11.2010
comment
@he_the_great: Я так не думаю. Проблема с достаточно умными решениями для компиляторов заключается в том, что они часто не существуют. :П   -  person DK.    schedule 19.11.2010
comment
@he_the_great: Нет, не может. Как видите, для кэширования необходимо знать, когда данные изменились. Это может произойти только в том случае, если компилятор добавит логический флаг к моей матрице, как я сделал выше. Компилятору не разрешено добавлять произвольные данные в ваши структуры. Кроме того, как говорит ДК, он просто этого не делает. Меня не интересует мог.   -  person Peter Alexander    schedule 19.11.2010
comment
@Peter, нет, ему не нужно знать, когда данные изменяются, потому что вы каждый раз вызываете его с данными. Кэширование не будет в вашей структуре, а будет частью вызова функции, или это намерение. Поскольку компилятор не кэширует, я не предоставил это в качестве ответа. Но кэширование — это одна из целей наличия неизменяемых чистых функций.   -  person he_the_great    schedule 19.11.2010
comment
@he_the_great: Как вы можете кэшировать часть вызова функции? Если две разные функции, F и G, вызывают determinant() для одного и того же Matrix без изменений в матрице, как они могут независимо знать, что могут использовать одно и то же кэшированное значение?   -  person Peter Alexander    schedule 19.11.2010
comment
Это не был бы детерминант(), который был бы кэширован. Если вы вызвали что-то вроде calc_m_determinant(int i, int j, double x) pure {}, то эта функция может быть кэширована для предыдущих вызовов на основе переданных значений. Я не знаю, как это должно быть сделано, но это было упомянуто как нечто возможное.   -  person he_the_great    schedule 21.11.2010
comment
Это невозможно сделать, по крайней мере, без довольно значительных накладных расходов памяти. И кроме того, как я уже говорил, меня не особенно интересует, что какой-нибудь волшебный компилятор мог бы сделать — ни один компилятор этого не делает, и это все, что имеет значение.   -  person Peter Alexander    schedule 21.11.2010
comment
Введение закона Годвина в языковые дебаты; Тип кэширования IIRC, который рассматривается, был доступен в Lisp на протяжении десятилетий.   -  person BCS    schedule 22.11.2010
comment
почему вам нужно, чтобы determinant() было константой?   -  person Alexander Malakhov    schedule 02.12.2010
comment
Потому что я хочу, чтобы функции с постоянными ссылками на матрицу могли найти ее определитель.   -  person Peter Alexander    schedule 02.12.2010


Ответы (5)


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

Константа D не является логической константой. Оно транзитивно и полностью константно. Язык технически не поддерживает логическую константу. Язык не определяет способа изменения константного объекта.

И на самом деле в С++ тоже нет логической константы. Использование mutable и отбрасывание const-ness позволяет вам полностью обойти const, так что, с технической точки зрения, const на самом деле ничего не гарантирует, кроме того, что вы не вызываете никаких неконстантных функций для константной переменной. Тот факт, что константные функции на самом деле являются константными и не связаны с вашими переменными, полностью поддерживается соглашением. Теперь большинство программистов не отбрасывают константность влево и вправо и делают все изменяемым, поэтому на практике это весьма полезно, но его можно не только полностью обойти, но и язык специально дает вам определенные средства для этого. . В C++ mutable и отбрасывание const четко определены и поддерживаются языком.

Д так не делает. Константа D на самом деле константа. Отбрасывание const для переменной и последующее ее изменение не определено. Изменяемого нет. У const D есть реальные гарантии (пока вы не делаете ничего неопределенного, например, отбрасываете const на что-то, а затем мутируете). Это важно не только потому, что гарантии компилятора для D намного сильнее, чем для C++, но и потому, что неизменяемые переменные нельзя изменить каким-либо образом форму или форму. Они могут находиться в памяти только для чтения, и кто знает, какие ужасные вещи произойдут, если вы попытаетесь отказаться от неизменяемости и изменить такую ​​переменную (segfault, вероятно, будет самым приятным, что может случиться). А поскольку константная переменная на самом деле может ссылаться на неизменяемые данные, отбрасывание const для изменения переменной или разрешение каким-либо образом изменять константные переменные было бы, по меньшей мере, плохо. Так язык не позволяет.

Теперь, как указывает BCS, D является прагматичным языком. Вы можете отказаться от const, после чего вы можете изменить переменную. Так, например, у вас может быть переменная, которая использовалась для кэширования возвращаемого значения функции const (предположительно, с тем, что этот кеш становится недействительным, если состояние объекта изменилось) и отбросить const, чтобы изменить его. Пока рассматриваемая переменная на самом деле не является неизменной, она будет работать. Однако это неопределенное поведение. Как только вы это сделаете, вы сами по себе. Вы обходите систему типов и гарантии компилятора. Вы несете ответственность за то, чтобы не сделать это с неизменяемым объектом или иным образом не испортить то, что компилятор обычно гарантирует. Итак, если вам нужно это сделать, вы можете, но вы выходите на Дикий Запад, и вы должны убедиться, что вы не мутируете то, что не должны.

Учитывая, что отбрасывание const будет работать до тех пор, пока переменная на самом деле не ссылается на неизменяемые данные, можно создать шаблон Mutable, чтобы по существу получить то, что mutable дает вам в C++ (таким образом, он будет выполнять отбрасывание const -ness для вас). he_the_great приводит пример такого шаблона в своем ответе. Но использование такого шаблона по-прежнему является неопределенным поведением. Использование его на объекте, который на самом деле неизменяем, вызовет проблемы. Вы, программист, должны убедиться, что он используется правильно.

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

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

person Jonathan M Davis    schedule 01.12.2010
comment
Учитывая, что отбрасывание const будет работать до тех пор, пока переменная на самом деле не ссылается на неизменяемые данные - это дано? Я знаю, что это, вероятно, верно в текущих компиляторах, но я почти уверен, что это все еще неопределенное поведение, что делает его невозможным, насколько я понимаю. - person Peter Alexander; 01.12.2010
comment
Это данность для dmd. Я был бы очень удивлен, если бы он никогда не работал ни с одним компилятором D. Однако он не не определен. Итак, ожидается, что он будет работать, и он должен работать, но с технической точки зрения ничто не гарантирует, что он всегда будет работать для всех компиляторов D. Но если вы хотите изменить переменную-член константного объекта, это единственный способ сделать это. - person Jonathan M Davis; 02.12.2010
comment
Ну, где-то говорят о возможности автоматического распараллеливания с использованием гарантий, которые предоставляет const. Лично я не думаю, что это возможно, но если это возможно, а это действительно происходит, то я предпочел бы, чтобы моя программа осталась нетронутой, когда будут внесены изменения. - person Peter Alexander; 02.12.2010
comment
Const на самом деле не дает достаточных гарантий для многого на этом фронте (за исключением, возможно, в сочетании с pure), но, как правило, он неизменяем, что вам нужно при работе с несколькими потоками. Но если бы ваши функции были действительно логическими константами, фактор чистоты не должен был бы создавать проблемы, поскольку результат все равно не изменился бы. Учитывая природу const, трудно поверить, что отбрасывание const для объекта в изменяемой памяти когда-нибудь перестанет работать (особенно, если он не чистый), но если вы хотите избежать даже такой возможности, то имеет смысл избегать отбрасывания const. - person Jonathan M Davis; 02.12.2010
comment
Почему вы рекламируете, что const в D обеспечивает надежные гарантии, в отличие от C++ (документация) const, и затем рекламируете способ обойти это? Конечно, как указал Питер Александер, необходимо lconst (или какой-либо тип логической константы). Я считаю, что свойства константности функций становятся довольно сложными (например, иногда D нужны неконстантные, константные и неизменяемые версии одной и той же функции) и не должны управляться программистом, хотя, конечно, у нас нет альтернатива. - person dhardy; 05.10.2011
comment
Существуют всевозможные трудности с предоставлением логической константы с какими-либо реальными гарантиями, поэтому вероятность того, что что-либо подобное будет добавлено к самому языку, невелика. Программист уже может сделать это самостоятельно в том смысле, что он может пометить функцию как логически константную в документации, но сделать ее изменяемой. И я не утверждаю, что кто-то должен использовать внешнюю переменную, чтобы обойти const — хотя бы потому, что это нарушает чистоту. Я просто указываю, что если вы хотите нарушить чистоту, это это способ кэширования. - person Jonathan M Davis; 05.10.2011

Я не прикасался к D2 целую вечность, так что вы можете перепроверить то, что я говорю. :)

Я не уверен, что у вас действительно есть хорошие варианты. const и immutable в D значительно сильнее, чем в C/C++, поэтому их отбрасывание не вариант. Вы явно исключили изменение использования const в своем коде.

Вы можете кэшировать результат операции в глобальной хэш-таблице с ключом на самом значении матрицы. Это будет работать для любой комбинации const/immutable. Проблема с этим, конечно, в том, что D не имеет самых быстрых хеш-таблиц в мире, и вычисление хэша может быть медленным. Возможно, предварительно вычислить хэш при создании матрицы.

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

Кроме этого, я не могу думать ни о чем другом. Проблема, на самом деле, в том, что вы просите компилятор защитить вас с помощью const, а затем пытаетесь вырваться из этого. «Правильное» решение, вероятно, просто не использовать const. :П

person DK.    schedule 19.11.2010
comment
Вот чего я опасаюсь: D может быть настолько сильным, что станет непригодным для использования, поэтому вместо слабых гарантий C++ у вас вообще нет никаких гарантий, потому что никто не может его использовать (Фобос даже не использует его) - person Peter Alexander; 19.11.2010
comment
@Peater: я бы не согласился с тем, что это непригодно для использования. Проблема возникает только тогда, когда что-то в конечном итоге определяется как константа, хотя на самом деле это не константа. - person BCS; 19.11.2010
comment
Но из-за вирусного эффекта константы, превращение одной функции в неконстантную обычно делает множество функций неконстантными. Как только determinant() становится неконстантным, все функции, которые вызывают его в константной матрице, должны измениться, чтобы использовать неконстантную матрицу, что означает, что все функции, которые передают неконстантную матрицу тем функциям теперь нужна неконстантная матрица и т. д. и т. д. Она распространяется по всей кодовой базе, и удачи в попытках написать универсальные функции… - person Peter Alexander; 20.11.2010
comment
@Peter: Я думал, что это то, что я сказал. В вашем примере единственная причина сделать determinant() неконстантным - это если вы хотите, чтобы он делал что-то с экземпляром, чего нельзя сделать с константным экземпляром; то есть экземпляр на самом деле не является константой. В этот момент у вас есть что-то, определяемое как константа, хотя на самом деле это не константа; таким образом, вы находитесь именно в том случае, когда я сказал, что возникает проблема. - person BCS; 20.11.2010
comment
Все изменится, если вы добавите логическую константу, но IIRC мало что может получить, если использовать ее как языковую конструкцию, которую нельзя получить из реализации библиотеки. Гарантия, которую можно получить от языковой версии, слишком слаба. - person BCS; 20.11.2010
comment
Однако дело в том, что determinant() все еще const -- это просто другой тип const (логическая константа против двоичной константы). Для пользователей определителя нет никакой разницы между логической и бинарной версиями const (вызов определителя фактически не меняет никакого наблюдаемого состояния). - person Peter Alexander; 20.11.2010
comment
Кстати, что вы подразумеваете под логической константой в качестве реализации библиотеки? - person Peter Alexander; 20.11.2010
comment
Вы можете создать тип LogicalConstMatrix, который может быть создан из Matrix, но не предоставляет никаких логически изменяемых методов. Не очень удобно, но главное здесь то, что попытка втиснуть логическую константу в двоичную константу D2 просто не сработает. - person DK.; 20.11.2010
comment
@Peter функция определителя кэширования не является константой, потому что она изменяет объект. Это может быть логическая константа, но это нечто другое. Что касается логической константы библиотеки: должна быть возможность создать тип прокси-объекта, который кэширует любые методы, которые вы хотите. -- Также не могли бы вы использовать функцию @username? blog.stackoverflow.com/2010/01/new-improved-comments-with-reply Это облегчает людям поиск ваших ответов. - person BCS; 21.11.2010

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

class M {
  bool set;
  real val;

  real D() const {
    if(!set) {
      M m = cast(M)this;
      m.val = this.some_fn();
      m.set = true;
    }
    return this.val;
  }
}
person BCS    schedule 19.11.2010
comment
Если я помню спецификацию, результат вышеизложенного явно не определен. Проблема в том, что он может быть неизменным в постоянной памяти. Причина, по которой он не определен, заключается в том, что он вполне может привести к сбою вашей программы. Изменить: ‹digitalmars.com/d/2.0/const3. html› явно указывает, что отбрасывание const или immutable возможно, но недопустимо. - person DK.; 19.11.2010
comment
Да, это не определено, потому что в любой ситуации может быть разумно изменить или в постоянной памяти. Однако, если вы можете убедиться, что данные будут изменяемыми, то есть у вас не будет неизменяемых ссылок на ваш класс и вы будете вызывать D(), тогда это не будет проблемой. - person he_the_great; 19.11.2010
comment
Проблему с памятью только для чтения можно обойти, сделав невозможным создание типа во время компиляции. Это заставит все экземпляры читать/записывать память, и все готово. В качестве альтернативы, сделайте так, чтобы путь построения во время компиляции предварительно вычислял значение для кэширования, после чего код никогда не будет отбрасывать const для всех экземпляров, которые попадают в память только для чтения. - person BCS; 20.11.2010
comment
Патологический случай: вручную разместить объект на новой странице памяти, а затем приказать ОС защитить страницу от записи. Та-да: рабочее ПЗУ. :D - person DK.; 20.11.2010
comment
@DK: В этот момент вы выходите далеко за рамки того, на что когда-либо заявляла система типов. - person BCS; 21.11.2010

Я настоятельно рекомендую ответ BCS, поскольку он прост и безопасен, если не создается неизменяемая/константная матрица.

Другим вариантом, который помогает сделать даже неизменяемые/константные объекты действительными, является этот изменяемый шаблон. Или, по крайней мере, это намерение. Есть комментарии по поводу беспокойства.

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

struct Matrix
{
    double determinant() const
    {
        if ( *m_dirty )
        {
            *m_determinant = 646.363; /* expensive calculation */;
            *m_dirty = false;
        }
        return *m_determinant;
    }

    void set(int i, int j, double x) { *m_dirty = true; }

    Mutable!(bool*) m_dirty;
    Mutable!(double*) m_determinant;
};
person he_the_great    schedule 30.11.2010

Чтобы имитировать логическое const из C++ в D, вы можете использовать наследование для классов:

class ConstMatrix
{
public:
    double det() { // not marked as const!
        /* ... caching code ... */
    }
    /* ... the rest of the logically const interface ... */
}

class Matrix : ConstMatrix
{
public:
    void set( int row, int col, double val ) {
        /* ... */
    }
    /* ... the non-logically const interface ... */
}

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

Для структур вы можете использовать технику alias, чтобы сделать то же самое:

struct ConstMatrix 
{
    /* logically const bla bla blub */
}

struct Matrix
{
public:
    alias m this;

    /* non-const bla bla blub */
private:
    ConstMatrix m;
}

Для типов классов вы можете построить другие classes или structs с правильностью логической константы следующим образом:

class ConstBiggerClass
{
private:
    ConstMatrix m;
}

class BiggerClass : ConstBiggerClass
{
private:
    Matrix m;
}

Таким образом, компилятор проверит const правильность за вас. Однако вам понадобится дополнительный элемент данных. Для элементов типа class в типах class альтернативой может быть предоставление функции-члена, которая возвращает элемент данных с правильной константностью:

class ConstBiggerClass
{
public:
    void someLogicallyConstFunction( /*...*/ ) { /* ... */ }
protected:
    abstract ConstMatrix getMatrix();
}

class BiggerClass : ConstBiggerClass
{
public:
    void someMutableFunction( /*...*/ ) { /*...*/ }
protected:
    Matrix getMatrix() { return m; }
private:
    Matrix m;
}
person Ralph Tandetzky    schedule 15.05.2013