понедельник, 5 февраля 2024 г.

[prog.c++] Захотелось в C++ странного (на тему транзитивной константности)...

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

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

Что такое транзитивная константность?

Представим себе, что у нас есть:

class Foo {
public:
  void foo(); // Это не-const метод.
  void foo2() const// А это уже const-метод.
};

class Bar {
  Foo * m_foo; // Это не-const указатель!
public:
  ...
  void bar2() const { // Это const-метод, в котором мы не можем присвоить m_foo новое значение.
    m_foo->foo(); // Но зато можем вызвать не-const метод для m_foo.
  }
};

Foo foo;
const Bar bar{&foo};
bar.bar2(); // Этот вызов может изменить foo.

Из-за того, что в C++ константность не транзитивна, то в const-объекте bar можно иметь не-const указатель на foo и в const-методе Bar::bar2 можно поменять объект foo.

Если бы константность была транзитивной, то в Bar::bar2 указатель Bar::m_foo автоматически бы стал константным и вызвать в Bar::bar2 не-const метод Foo::foo у нас уже не получилось бы.

Поскольку в С++ транзитивной константности нет, то я было попробовал сделать ее вручную. По типу чего-то такого:

template<typename T>
class AutoConstPtr {
  T * m_ptr;
public:
  ...
  [[nodiscard]] T * get() { return m_ptr; } // Не-const.
  [[nodiscard]] const T * get() const { return m_ptr; } // Уже const.
};

Это позволяет получить транзитивную константность в простом случае:

class Bar {
  AutoConstPtr<Foo> m_foo; // Это уже не raw pointer.
public:
  ...
  void bar2() const {
    m_foo.get()->foo(); // А вот здесь будет ошибка компиляции!
  }
};

И это уже было именно то, что мне нужно. И, казалось бы, счастье было уже так близко...

Но, к сожалению, это не сработало на практике. Например, из-за вот таких случаев:

void ProcessItems(const std::vector<AutoConstPtr<Foo>> & items) {
  for(auto p : items) {
    p.get()->foo(); // Упс!
  }
}

Фокус в том, что p -- это будет копия AutoConstPtr<Foo>. Не-const копия. Следовательно, для p будет вызываться не-const версия get. Следовательно, будет возвращаться не-const указатель на Foo. Следовательно, можно вызывать не-const методы Foo, т.е. модифицировать Foo. И это в ситуации, когда исходно у нас были как раз константные указатели на Foo (ведь у нас const-ссылка на вектор указателей).

Вот таким вот незамысловатым образом красивая идея накрылась медным тазом. Абыдна, да 🙁

И вот тут мне захотелось, чтобы в C++ при описании конструктора можно было бы явно описать, применяется ли этот конструктор для const-объекта или нет.

Ведь сейчас мы пишем что-то вроде:

MyClass::MyClass(const MyClass & other) {...}

и понимаем, что это конструктор копирования. Но не понимаем, какой именно экземпляр MyClass при этом конструируется. Т.е.:

MyClass source{...};

MyClass copy1{source}; // Вызов конструктора копирования.
const MyClass copy2{source}; // Вызов того же самого конструктора копирования.

А что, если бы мы могли добавлять const и к конструктору?

template<typename T>
class AutoConstPtr {
  T * m_ptr;
public:
  ... // Здесь какие-то другие конструкторы.
  AutoConstPtr(const AutoConstPtr &) = delete// Нельзя построить не-const из const.
  AutoConstPtr(const AutoConstPtr & other) const // Тут все OK.
    : m_ptr{other.m_ptr}
  {}
  ...
};

Тогда бы не получилось бы скомпилировать конструкцию:

for(auto p : items) ...

потому что нельзя построить не-const объект AutoConstPtr из const-объекта.

А вот так бы получилось бы:

for(const auto p : items) ...

Вот такая вот странная фантазия.

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

const auto std::vector<AutoConstPtr<Foo>> & source = ...;
std::vector<AutoConstPtr<Foo>> selected;
std::copy_if(source.begin(), source.end(),
    std::back_inserter(selected),
    [](const auto & item) { return ...; });

Так что, возможно, идея транзитивной константности в принципе не для C++.

четверг, 1 февраля 2024 г.

[life.cinema] Очередной кинообзор

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

Фильмы

День мертвых (2021). Отличный пример кино, в котором минимум персонажей, минимум событий, а все держится на разговорах, но при этом следить за происходящим интересно. Хотя, наверняка, зайдет этот фильм далеко не всем.

Общество снега (La sociedad de la nieve, 2023). Впечатляющая история, снято все красиво, подача материала необычная... Но вот чего-то мне сильно не хватило. Не шедевр, к сожалению. А очень жаль. Тем не менее, имеет смысл смотреть.

Семейный план (The Family Plan, 2023). На удивление неплохо. Вполне можно посмотреть когда хочется отключить мозги и развлечься.

Догмен (Dogman, 2023). Если смотреть на этот фильм как на фэнтезино-фантастическую картину, вроде "Джокера", то в рамках этого жанра еще ничего, вполне смотрибельно.

Каменщик (The Bricklayer, 2023). Ну такое себе, на троечку. Хотя есть там что-то от духа боевиков 1980-х годов.

Дворец (The Palace, 2023). Не смог толком оценить. Вроде бы снято круто, вроде бы все закручивается и закручивается и в финале должен случиться апупей с апупеозом... Но заканчивается кино каким-то невнятным пшиком.

Озеро диких гусей (Nan fang che zhan de ju hui, 2019). Сам фильм мне не зашел, но в чем-то это оказалась любопытная картина, т.к. очень уж сильно отличается от европейского, не говоря уже про американское, кино.

Мятежная Луна, часть 1: Дитя огня (Rebel Moon - Part One: A Child of Fire, 2023). Попытка Netflix-а заполучить свою фэнтезийно-космическую франшизу. Получилась редкая муть (что-то по типу Восхождение Юпитер). Только, в отличии от "Восхождения Юпитер" здесь, помимо всего прочего, меня еще и визуальная составляющая раздражала (как и в предыдущей большой работе Зака Снайдера).

Сериалы

Медленные лошади (Slow Horses, третий сезон, 2023). Бодренько и динамично. Мне понравилось.

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

Мертвый сезон (Hors Saison, первый сезон, 2022). Ну такое себе. В принципе, глянуть можно, но слишком часто придется восклицать "да что за фигня?!"

Маяк 23 (Beacon 23, первый сезон, 2023). Могло бы что-то получиться, если бы авторы в первом сезоне хоть сколько нибудь законченную историю рассказали. А то оборвали там, где ожидалась развязка. С явным приглашением подождать следующего сезона. Но это-то как раз и создает ощущение обманутых ожиданий и сильно разочаровывает.

Не смог досмотреть

Легенда о самбо (2022). Смог осилить всего минут двадцать. Показалось, что это редкостное говно не смотря на местами красочную и качественную картинку.

пятница, 26 января 2024 г.

[prog.c++] Интересное чтиво про strict aliasing rule...

...лежит здесь: What is the Strict Aliasing Rule and Why do we care?

Документ далеко не новый, но если вы не в теме, то он будет, безусловно, полезен.

Из того, что стало откровением и открытием лично для меня (применительно к C++):

  • в C++ содержимое другого объекта можно просматривать путем каста к char, unsigned char или std::byte. Т.е., грубо говоря, вы всегда можете сделать reinterpret_cast<char *>(other_pointer). Но это не распространяется на signed char. И отдельная история с std::int8_t/uint8_t: эти типы могут быть простыми синонимами для char и unsigned char, а могут быть и отдельными, самостоятельными типами (в таком случае на них правило char/unsigned char/std::byte не распространяется);
  • оказывается, современные компиляторы могут понять, что делает std::memcpy и избавиться от реального вызова std::memcpy. Например, вот такой корректный способ получить float из int-а (при условии их одинаковых размеров):

    int src = ...;
    float dst;
    std::memcpy(&dst, &src, sizeof(src));

    в случае умного компилятора будет просто помещать в dst нужно значение без вызова memcpy;

  • не знал раньше про common initial sequence. А это, как выяснилось, может стать архиважной штукой при работе с union:

    struct A { char type; ... };
    struct B { char type; ... };
    struct C { char type; ... };
    union U { A a; B b; C c; };
    
    U u;
    u.a = ...;
    if(u.b.type == ...) // (1)
    {...}

    Обращение к u.b в точке (1) легально не смотря на то, что U::b -- это неактивный в данный момент элемент union-а.

понедельник, 22 января 2024 г.

[prog.c++] Оказывается, в современном C++ параметры шаблона с дефолтными значениями можно располагать в начале списка параметров шаблона...

Был приятно удивлен тому, что вот это вполне себе компилируется и работает так, как мне и нужно:

#include <iostream>

struct no_size_limit {
    static bool is_size_valid(std::size_t /*size*/) {
        std::cout << "no_size_limit::ensure_valid_size" << std::endl;
        return true;
    }
};

template<typename Size_Limiter=no_size_limit, typename... Args>
void f(Args && ...args) {
    if(Size_Limiter::is_size_valid(sizeof...(args))) {
        std::cout << "processing of args" << std::endl;
    }
    else
        std::cout << "ignoring args" << std::endl;
}

template<std::size_t N>
struct at_least {
    static bool is_size_valid(std::size_t size) {
        std::cout << "at_least<" << N << ">::ensure_valid_size" << std::endl;
        return (N <= size);
    }
};

int main() {
    f(12345);
    f<at_least<3>>(12345);
    f<at_least<5>>(123);
}

Цынк

Так-то я со времен C++98 привык, что параметры шаблона со значениями по умолчанию идут в конце списка параметров шаблона. А тут потребовалось, чтобы они шли в начале. И оно раз и заработало.

Приятно.


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.

вторник, 16 января 2024 г.

[linux] Если вам потребовался ArchLinux в Docker с пакетом из AUR...

...то вот эти ссылки могут оказаться полезны. По крайней мере мне помогли.

Arch_User_Repository. Официальная информация о том, что такое AUR и как ставить пакеты из AUR. Имеет смысл просмотреть хотя бы по диагонали, чтобы понимать, что к чему и почему.

Testing our package build in the Docker world. В принципе, основная статья, в которой вроде бы все собрано воедино в нормальном, лаконичном и более менее понятном виде.

Testing an Arch Linux package in Gitlab CI. Статья не совсем про Docker, но мне она оказалась наиболее полезна, т.к. там расписывается что и зачем делается.

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

ЗЫ. Т.к. после экспериментов с Docker-ом остается куча всяких устаревших (и не очень) образов, то найти простые способы поудалять лишние Docker-овские images и containers можно здесь: How To Remove Docker Images, Containers, and Volumes. Например:

$ docker image prune
$ docker rmi $(docker images -a -q)
$ docker rm $(docker ps -a -f status=exited -q)
$ docker stop $(docker ps -a -q)
$ docker rm $(docker ps -a -q)

среда, 10 января 2024 г.

[prog.c++.wtf] Публичный член приватного вложенного типа?

Еще одно открытие для меня в языке C++, которым пользуюсь уже больше 30 лет:

class Outer {
    struct Inner {
        int m_a{};
        int m_b{};
    };
public:
    Inner m_i;
};

int main()
{
    Outer o;
    o.m_i.m_a = 3;
    o.m_i.m_b = 4;
}

Оказывается, так можно. Цынк.

Что меня выморозило в этом примере: мы же класс Inner сделали закрытым вложенным классом для Outer. Т.е. вроде как, по логике вещей, классом Inner могут пользоваться только сам Outer и его друзья.

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

Впервые с таким столкнулся. Я, честно говоря, ожидал, что компилятор не должен позволить объявить публичный Outer::m_i. Но, в очередной раз, ошибся 🥴


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.

вторник, 9 января 2024 г.

[prog.c++] Оказывается, в современном C++ нельзя взять и сложить std::string с std::string_view...

На пятый год работы с C++17, в котором std::string_view появился, "Зоркий глаз" (т.е. я) заметил, что в C++ пока нет версии operator+ для случая std::string и std::string_view :(

Поэтому ни в C++17, ни в C++20, ни, подозреваю, в C++23, не получится написать так:

std::string f(std::string_view a, std::string_view b) {
  using namespace std::string_view_literals;
  return std::string{"Expected value: "} + a + ", actual value: "sv + b;
}

Но есть пропозал. И, может быть, нам повезет и в C++26 эта фича в языке таки появится. А может только в C++29...

Если честно, то я, мягко говоря, в шоке.


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.