Ответы пользователя по тегу C++
  • Как использовать потоки в современном C++ в приложении на основе цикла событий?

    @code_panik
    В сущности, async - это thread + promise/future с удобствами: его не нужно join-нить руками, не нужно создавать промежуточный promise, а возвращенный функцией результат и исключения можно получить в вызывающем коде. При этом деструктор объекта future, полученного в результате вызова async, блокирует вызывающий поток до завершения нашей async-функции.
    В нашем случае разницы с использованием thread в части отправки сигналов нет, ведь нам по-прежнему придется сигнализировать виджету о завершении сканирования. Это можно сделать в самой функции сканирования. Если я правильно понял, раньше мы это делали в функции scanFinished, которая вызывалась в конце функции сканирования.
    Под задачами обычно понимают независимые потоки выполнения (необязательно параллельно), которые можно запустить и забыть, а в конце забрать результат. Потоки сканирования директорий, судя по коду из вопроса, не зависят друг от друга, и мы наш ThreadManager можем разбить на две переменные типа ScanTask

    #include <iostream>
    #include <future>
    #include <atomic>
    #include <memory>
    #include <string>
    
    class ScanTask {
    public:
    	struct Result {
    		enum {
    			Success, Failure, Canceled
    		} status;
    		std::string message;
    	};
    
    	ScanTask(const std::string& dir)
    		: canceled_{ false },
    		result_{std::async(&ScanTask::scan, this, dir)}
    	{ }
    
    	void cancel() {
    		canceled_ = true;
    	}
    
    	Result result() { 
    		return result_.get(); // result_.valid() == false
    	}
    
    	Result scan(const std::string& dir) {
    		struct Finalize {
    			~Finalize() {
    				// emit signal scan completed
    			}
    		} finalizeSignal;
    
    		while (!canceled_ /* && not done */) {
    			// .. scan subdirectories
    			/*
    			if (something failed)
    				return {Result::Failure, "Failure"};
    			*/
    		}
    		if (canceled_)
    			return { Result::Canceled, std::string("[Canceled] ") + dir };
    		return { Result::Success, std::string("[Success] ") + dir };
    	}
    
    private:
    	std::atomic<bool> canceled_;
    	std::future<Result> result_;
    };
    
    int main() {
    	auto localScanTask = std::make_unique<ScanTask>("a");
    	localScanTask->cancel();
    	std::cout << localScanTask->result().message << std::endl;
    
    	auto sharedScanTask = std::make_unique<ScanTask>("b");
    	sharedScanTask->cancel();
    	std::cout << sharedScanTask->result().message << std::endl;
    	return 0;
    }

    Можно было даже не создавать отдельный класс, а в функции-обработчике нажатия кнопки запуска присваивать future члену виджета результат вызова async от анонимной лямбды, как предлагали с thread в ревью. При этом atomic canceled_ можно захватить по ссылке, она была бы общей для всех.
    Ответ написан
    Комментировать
  • Как переделать код под ООП?

    @code_panik
    Сначала замечу, что нет никакого смысла переписывать код в стиле ООП, потому что класс, назовем его Calculator, не имеет состояния, не хранит данных, состоит только из функций. То есть весь код будет выглядеть как раньше, но придется приписывать в начало операций имя объекта класса типа Calculator, напр. calculator.divide(1, 2);. Достаточно собрать эти функции в нашем пространстве имен my_math, например,
    namespace my_math {
    bool operator<(const std::string& st1, const std::string& st2) {
        size_t len1 = st1.length();
        size_t len2 = st2.length();
        if (len1 != len2) return (len1 < len2);
        long int i = 0;
            // ищем разряд, в котором значения отличаются
        while (i < len1 && st1[i] == st2[i])
        {
            i++;
        }
            // если разряд найден, то меньше число с меньшей цифрой для положительных и с большей цифрой для отрицательных, иначе числа равны
        return (i < len1) && ((st1[i] < st2[i]) );
    }
    bool les(int lhs, int rhs) {
        return lhs <= rhs;
    }
    }
    
    int main() {
        using my_math::operator<; // иначе cout << my_math::operator<(s1, s2) << endl;
        std::string s1 = "1", s2 = "2";
        cout << (s1 < s2) << endl;
    
        int a = 1, b = 2;
        cout << (my_math::les(a, b)) << endl;
        return 0;
    }

    Для ООП можно воспользоваться примером https://godbolt.org/z/rEPzdKe4j. В нем определены два файла: calculator.h и calculator.cpp с объявлением класса и реализацией соответственно.
    Замечу, что в реализациях бинарных операций не следует передавать string по значению, чтобы избежать лишних копирований. Если заменить тип аргумента std::string на ссылочный const std::string&, придется поправить код, чтобы он не изменял значения аргументов.
    Ответ написан
    Комментировать
  • C++ | Почему новый поток со временем периодически перестаёт работать?

    @code_panik
    Наиболее вероятная проблема - race condition (состояние гонки) + ub (неопределенное поведение) из-за проблем синхронизации основного потока, в котором вызывается деструктор ~MyClass, и потока thr_dPManager.
    Пусть в основном потоке вызывается деструктор, но exit_flag_dPManager = false, тогда this->thr_dPManager.join(); не выполнится (race condition) и без ожидания завершения run() в основном потоке будут тихо освобождены все данные связанные с объектом класса и объектом потока thr_dPManager. В run() мы попытаемся обратиться к освобожденным данным класса (ub) и получим непредсказуемое поведение программы. В c++ для каждого потока мы должны корректно вызывать join или detach. Вынесем join из if,
    MyClass::~MyClass()
    {
        if (this->exit_flag_dPManager)
            this->exit_flag_dPManager = false;
    
        if (thr_dPManager.joinable())
            thr_dPManager.join();
    }

    Осталось проверить логику программы и убедиться, что "бесконечный" цикл завершится. Пока непонятно, зачем в деструкторе строка this->exit_flag_dPManager = false, ведь после завершения деструктора у нас не будет доступа к данным класса.
    Если exit_flag_dPManager используется для синхронизации: завершить run, если завершается основной поток, — тогда можно писать
    MyClass() : exit_flag_dPManager(false) ....
    
    void run() {
      while (!exit_flag_dPManager.load()) { ... }
    }
    
    ~MyClass() {
      this->exit_flag_dPManager = true;
    
      if (thr_dPManager.joinable())
        thr_dPManager.join();
    }


    Также нужно иметь в виду потенциальные проблемы из статьи https://habr.com/ru/articles/444464/.
    Ответ написан
    Комментировать
  • C++. Почему в консоли выводит непонятные символы, хотя setlocale(LC_ALL, &amp;quot;RU&amp;quot;) прописан?

    @code_panik
    Если кратко, то у windows беда с многообразием представлений символов (кодировок) в терминале. Есть однобайтовые 866 (стандартная терминала), 1251 (ansi), есть многобайтовая 65001 (utf-8). Чтобы проверить кодировку, в терминале наберите chcp. Если нет особых требований к работе с кодировками, достаточно обычного подхода
    #include <iostream>
    #include <string>
    using namespace std;
    
    int main()
    {
      string words;
      cin >> words;
      cout << words;
      return 0;
    }

    Пример работает с 866 и 1251.
    Если нужна кодировка 1251 (и она не выбрана для терминала по умолчанию при запуске), запускайте программу из подготовленного окна терминала. Для этого в терминале наберите сначала однажды chcp 1251. Кодировка может понадобиться, если в исходном коде программы в кодировке 1251 будут строки с кириллицей.
    Ответ написан
    Комментировать
  • Какие виды функций есть в C++, и как их различать?

    @code_panik
    В c++ есть бинарный оператор <<, который для целых чисел является оператором сдвига битов числа влево на заданное количество позиций (то же, что и повторное умножение на два). Например, программа напечатает 2 << 1 == 4,
    #include <iostream>
    using namespace std;
    
    int main() {
        cout << "2 << 1 == " << (0b010 << 1); // 0b100
        return 0;
    }

    Одна из основных возможностей языка - перегрузка функций, в частности встроенных операторов (не всех). То есть мы можем определить свой тип данных со своей реализацией оператора <<.
    #include <iostream>
    using namespace std;
    
    struct Foo {
        Foo& operator<<(int x) {
            cout << "Integer: " << x << '\n';
            return *this;
        }
    
        Foo& operator<<(const char* str) {
            cout << "String: " << str << '\n';
            return *this;
        }
    };
    
    int main() {
        Foo foo;
        foo << 3; // Integer: 3
        foo.operator<<("Hello world"); // String: Hello world
        return 0;
    }

    Для компилятора запись foo << 3; значит именно вызов функции foo.operator<<(3). Аналогично в случае cout, объекта типа std::ostream, который представляет поток вывода (character output).
    https://en.cppreference.com/w/cpp/io/basic_ostream...
    Ответ написан
    Комментировать
  • Как в c++ сохранить txt файл в кодировке ansi?

    @code_panik
    Под ANSI обычно подразумевается однобайтовая кодировка Windows-1251.
    Синонимы: CP1251; ANSI (только в русскоязычной ОС Windows).

    Символ типа char в C++ всегда имеет размер 1 байт, поэтому работаем с файлом самым обычным образом. Прочитанное содержимое (возможно части) файла можно сохранить в буфере типа std::string или массиве char. Файл в программе можно представить объектом потока: std::ifstream, std::ofstream, std::fstream. Выходной файл, в который запишем содержимое полученного таким образом буфера, будет иметь ту же кодировку ANSI.

    Проще всего прочитать всё содержимое файла в буфер и потом перезаписать файл полностью, например так.
    #include <fstream>
    #include <string>
    
     int main() {
    	 const std::string path = "<путь к файлу>";
    	 std::fstream file(path, std::ios::in);
    	 std::string text;
    	 std::getline(file, text, '\0');
    	 file.close();
    
    	 text += "[edit]\n";
    
    	 file.open(path, std::ios::out);
    	 file << text;
    	 return 0;
     }
    Ответ написан
    Комментировать
  • Как сделать коллизии для вращающейся фигуры SFML?

    @code_panik
    Upd: sfml не предоставляет средств для работы с физикой. Разрешение коллизий придется реализовать самому, пример.

    Нужно начать с того, что разрешение коллизий выполняется не для спрайта, который существует только для отображения объекта, а для связанной с объектом формы (множеством форм) - коллайдера (collider) (прямоугольника, окружности, параллелепипеда, сферы и т.п.). Коллайдер представляет собой упрощенную форму игрового объекта. Работа с такими формами значительно упрощает разрешение коллизий.

    В двумерном платформере коллайдером может быть прямоугольник. Если объект имеет квадратную форму, и мы можем пожертвовать столкновением с углами, то коллайдером можно выбрать аккуратно подогнанную по размеру окружность. Распространенный вид коллайдера - AABB (axis aligned bounding box), прямоугольник, стороны которого всегда параллельны осям координат. При повороте игрового объекта достаточно изменить размеры такого прямоугольника, чтобы объект оставался в его границах.

    Следующая за определением коллизий большая проблема - реализация реакции на коллизию. Если просто обнулить скорость в момент коллизии, объекты слипнутся, создавая бесконечную коллизию. Также нужно помнить о возможных длительных интервалах между полными итерациями игрового цикла, которые влияют на интервалы разрешения коллизий, если оно реализовано в потоке с игровой логикой или отрисовкой кадра, а не в отдельном потоке с постоянной частотой. Если интервал достаточно долгий, объекты могут проваливаться друг через друга, игрок может вылететь за рамки сцены.

    Если нужно реализовать именно игру, то посмотрите в сторону библиотек с готовыми реализациями. Если проект учебный, выберите простую форму коллайдера, например aabb или окружность, и поищите для них примеры разрешения коллизий. Примеров множество.
    Ответ написан
  • Как реализовать управление для двух игроков на одной клавиатуре SFML?

    @code_panik
    В самом простом случае можно реализовать так же, как реализовано управление одним игроком. Достаточно регистрировать события нажатия/отпускания клавиш. В sfml функция sf::Window::setKeyRepeatEnabled с аргументом false включает режим однократной регистрации нажатия клавиши. То есть событие keyPressed будет зарегистрировано однажды при нажатии клавиши, даже если она остается нажатой.

    Можем считать, что между событиями keyPressed и keyReleased клавиша остается нажатой. Сохраним нажатые клавиши с списке и на каждом шаге игрового цикла будем менять соответственно нажатым клавишам состояние игры, в том числе игрока.

    Пример реализации (C++17):
    #include <SFML/Graphics.hpp>
    #include <unordered_set>
    #include <unordered_map>
    
    using namespace std;
    
    void normalize(sf::Vector2i& v) {
    	int length = sqrt(1ll * v.x * v.x + 1ll * v.y * v.y);
    	if (length != 0) {
    		v.x /= length;
    		v.y /= length;
    	}
    }
    
    class Input {
    public:
    	using Keys = std::unordered_set<sf::Keyboard::Key>;
    
    	void registerKeyEvent(sf::Event e) {
    		if (e.type == sf::Event::KeyPressed)
    			pressedKeys_.insert(e.key.code);
    		else if (e.type == sf::Event::KeyReleased)
    			pressedKeys_.erase(e.key.code);
    	}
    
    	const Keys& pressedKeys() const {
    		return pressedKeys_;
    	}
    
    private:
    	Keys pressedKeys_;
    };
    
    class Player {
    public:
    	enum class Control {
    		moveUp, moveLeft, moveRight, moveDown
    	};
    
    	Player(sf::Vector2i initialPosition, sf::Color color)
    		: position_{ initialPosition } {
    		sprite_.setPosition(sf::Vector2f(initialPosition.x, initialPosition.y));
    		sprite_.setFillColor(color);
    		sprite_.setRadius(50);
    	}
    
    	Player& map(Control control, sf::Keyboard::Key key) {
    		keyControls_[key] = control;
    		return *this;
    	}
    
    	void process(const Input& input) {
    		velocity_ = {};
    		for (sf::Keyboard::Key k : input.pressedKeys()) {
    			if (auto it = keyControls_.find(k); it != keyControls_.end()) {
    				switch (it->second) {
    				case Control::moveDown:
    					velocity_.y = 1;
    					break;
    				case Control::moveUp:
    					velocity_.y = -1;
    					break;
    				case Control::moveLeft:
    					velocity_.x = -1;
    					break;
    				case Control::moveRight:
    					velocity_.x = 1;
    					break;
    				default:
    					break;
    				}
    			}
    		}
    		normalize(velocity_);
    		velocity_ *= speed_;
    	}
    
    	void move(sf::Vector2i position) {
    		position_ = position;
    		sprite_.setPosition(sf::Vector2f(position.x, position.y));
    	}
    
    	const sf::CircleShape& sprite() const {
    		return sprite_;
    	}
    
    	sf::Vector2i position() const {
    		return position_;
    	}
    
    	sf::Vector2i velocity() const {
    		return velocity_;
    	}
    
    private:
    	int speed_ = 300; // pixels / s
    	sf::Vector2i velocity_;
    	sf::Vector2i position_;
    	
    	sf::CircleShape sprite_;
    
    	std::unordered_map<sf::Keyboard::Key, Control> keyControls_;
    };
    
    class Game {
    public:
    	Game(sf::RenderWindow* gameWindow)
    		: window_(gameWindow) {
    		window_->setKeyRepeatEnabled(false);
    		auto width = window_->getSize().x;
    		auto height = window_->getSize().y;
    		Player mrRed(sf::Vector2i(width / 3, height / 3 * 2), sf::Color::Red);
    		mrRed.map(Player::Control::moveUp, sf::Keyboard::W)
    			.map(Player::Control::moveDown, sf::Keyboard::S)
    			.map(Player::Control::moveLeft, sf::Keyboard::A)
    			.map(Player::Control::moveRight, sf::Keyboard::D);
    		players_.emplace_back(std::move(mrRed));
    
    		Player mrBlue(sf::Vector2i(width / 3 * 2, height / 3 * 2), sf::Color::Blue);
    		mrBlue.map(Player::Control::moveUp, sf::Keyboard::Up)
    			.map(Player::Control::moveDown, sf::Keyboard::Down)
    			.map(Player::Control::moveLeft, sf::Keyboard::Left)
    			.map(Player::Control::moveRight, sf::Keyboard::Right);
    		players_.emplace_back(std::move(mrBlue));
    
    		Player mrGreen(sf::Vector2i(width / 2, height / 3), sf::Color::Green);
    		mrGreen.map(Player::Control::moveUp, sf::Keyboard::I)
    			.map(Player::Control::moveDown, sf::Keyboard::K)
    			.map(Player::Control::moveLeft, sf::Keyboard::J)
    			.map(Player::Control::moveRight, sf::Keyboard::L);
    		players_.emplace_back(std::move(mrGreen));
    	}
    
    	void update() {
    		processInputEvents();
    		sf::Time dt = clock_.restart();
    		processMovement(dt.asMilliseconds());
    		drawScene();
    	}
    
    private:
    	void processInputEvents() {
    		for (sf::Event e; window_->pollEvent(e); ) {
    			if (e.type == sf::Event::Closed) {
    				window_->close();
    				return;
    			}
    			input_.registerKeyEvent(e);
    		}
    		for (auto& player : players_)
    			player.process(input_);
    	}
    
    	void processMovement(sf::Int32 dt) {
    		for (int id = 0; id < players_.size(); ++id) {
    			auto& player = players_[id];
    			sf::Vector2i position = player.position() + sf::Vector2i(
    				player.velocity().x * 1e-3f * dt,
    				player.velocity().y * 1e-3f * dt
    			);
    			player.move(position);
    		}
    	}
    
    	void drawScene() {
    		window_->clear(sf::Color::White);
    		for (const auto& player : players_)
    			window_->draw(player.sprite());
    		window_->display();
    	}
    
    	std::vector<Player> players_;
    	Input input_;
    	sf::RenderWindow* window_;
    	sf::Clock clock_;
    };
    
    
    int main() {
    	sf::RenderWindow mainWindow(sf::VideoMode(600, 600), "Circles", sf::Style::Close);
    	mainWindow.setFramerateLimit(120);
    	Game game(&mainWindow);
    	while (mainWindow.isOpen())
    		game.update();
    	return 0;
    }


    Мы создаем три круга, которые могут двигаться независимо. Клавиши движений назначаются в конструкторе Game.

    FrameLimit ограничен, потому что скорость и другие результаты расчетов целочисленные и привязаны к пискелям. При слишком высокой частоте кадров изменения расстояний на каждом шаге симуляции будут незначительными, округлятся к 0. Лучше заменить целочисленную арифметику на float-арифметику, чтобы не было грубых ошибок округления как в normalize, и промежуточных преобразований sf::Vector.

    За поддержание актуальности списка нажатых клавиш отвечает class Input.
    Ответ написан
    6 комментариев
  • Почему автодополнение не предлагает мне поля структуры?

    @code_panik
    Это особенность работы IntelliSense в Visual Studio. Более того, если в коде
    template<typename T>
    struct Outer {
      struct Node {
        int data;
      };
      void foo() { node.data; }
      Node node;
    };

    навести курсор на data внутри функции foo, скорее всего, получим подсказку <unknown> Outer<T>::Node::data, где unknown - тип, который не удалось вывести.

    IntelliSense позволяет подставить в шаблонный параметр значение конкретного типа. Для этого значение опции Template IntelliSense: Tools > Options > C/C++ > Advanced > IntelliSense > Enable Template IntelliSense должно быть true. Демонстрация работы функции: 1, 2, 3.

    Для заданной подстановки IntelliSense сгенерирует у себя экземпляр шаблонного класса, и подсказки будут работать как в случае с не шаблонным.
    Ответ написан
  • Источник данных не найден и не указан драйвер. Как исправить?

    @code_panik
    Используйте подходящий драйвер для подключения к базе данных вместо QODBC. Для postgresql это QPSQL (список драйверов в qt 5.15).
    Note: You should use the native driver, if it is available, instead of the ODBC driver. ODBC support can be used as a fallback for compliant databases if no native driver is available.
    QODBC for Open Database Connectivity (ODBC)
    При подключении с помощью ODBC в setDatabaseName указывается не имя базы данных, а datasource name или данные подключения (пример по ссылке выше).
    When connecting to an ODBC datasource, you should pass the name of the ODBC datasource to the QSqlDatabase::setDatabaseName() function, rather than the actual database name.
    Ответ написан
    Комментировать
  • Можно ли выделить память определенного размера?

    @code_panik
    В C++ можно, как и в C, выделить себе участок памяти, в котором будут жить объекты (в широком смысле, в том числе встроенного типа, напр. int). Чтобы начать жизнь объекта, его нужно разместить в памяти с соответствующим типу выравниванием. Если выравнивание, равное степени двойки, будет меньше требуемого для типа, то приведение с помощью static_cast типа указателя в выделенной памяти (void*, char*, unsigned char*) к указателю на тип объекта создает неопределенное поведение. Значит, чтобы безопасно использовать объект, размещенный в выделенной памяти, нужно убедиться, что он размещен (с помощью placement new) по выровненному адресу.

    Пусть мы создаем основной буфер под объект вызовом new unsigned char[size]. Будет ли буфер иметь достаточное выравнивание, чтобы в начале его разместить BUFFER::a типа int? - Да, если мы зарезервируем сразу достаточно памяти,

    In addition, if the new-expression is used to allocate an array of char, unsigned char, or std::byte (since C++17), it may request additional memory from the allocation function if necessary to guarantee correct alignment of objects of all types no larger than the requested array size, if one is later placed into the allocated array.


    Выражения new, new[] автоматически определяют размер выделяемой памяти и передают его низкоуровневым функциям operator new, operator new[] соответственно. Память, выделенная new, освобождается вызовом delete, для new[] - delete[]. Поведение delete похоже на поведение new выше.

    Значит, у нас есть всё необходимое чтобы реализовать свой класс буфера. Например,
    #include <cstddef>
    #include <iostream>
    #include <cstring>
    
    class Buffer {
    public:
      using preamble_type = int;
      static constexpr std::size_t preamble_size = sizeof(preamble_type);
    
      Buffer(const preamble_type& value, std::size_t bufferSize) {
        buffer_ = new unsigned char[preamble_size + bufferSize];
        ::new(buffer_) preamble_type(value);
      }
    
      Buffer(const Buffer& other) = delete;
      Buffer& operator=(const Buffer& rhs) = delete;
    
      Buffer(Buffer&& other) noexcept
        : buffer_(other.buffer_) {
          other.buffer_ = nullptr;
        }
    
      Buffer& operator=(Buffer&& rhs) noexcept {
        std::swap(buffer_, rhs.buffer_);
        return *this;
      }
    
      virtual ~Buffer() {
        destroy();
      }
    
      preamble_type& preamble() noexcept {
        return const_cast<preamble_type&>(
            const_cast<const Buffer *>(this)->preamble()
            );        
      }
    
      const preamble_type& preamble() const noexcept {
        return *preambleData();
      }
    
      unsigned char* data() const noexcept {
        return buffer_ + preamble_size;
      }    
    
      void resize(std::size_t size) {        
        if (buffer_ != nullptr) {
          auto newBuffer = new unsigned char[preamble_size + size];
          ::new(newBuffer) preamble_type(std::move(preamble()));
          destroy();
          buffer_ = newBuffer;
        }
      }
    
    private:
      preamble_type* preambleData() const noexcept {
        // return std::launder(reinterpret_cast<preamble_type*>(buffer_)); c++17
        return reinterpret_cast<preamble_type*>(buffer_);
      }
    
      void destroy() noexcept {
        preambleData()->~preamble_type();
        delete[] buffer_;
      }
    
      unsigned char* buffer_ = nullptr;
    };
    
    int main()
    {
      using std::cout;
      using std::endl;
    
      const std::size_t bufferSize = 100;
      Buffer b(100, bufferSize);
      const char hello[] = "hello world!";
      memcpy(b.data(), hello, sizeof(hello));
    
      auto c = std::move(b);
      cout << c.preamble() << ' ' << c.data() << endl;
    
      b = Buffer(5, 20);
      memcpy(b.data(), hello, sizeof(hello));
      cout << b.preamble() << ' ' << b.data() << endl;
      return 0;
    }


    Из кода можно выкинуть всё, что касается preamble_type, если считать, что первое поле всегда будет int. С другой стороны, код можно сделать обобщенным с минимальными изменениями. Например,
    template<typename PreambleT>
    class Buffer {
    public:
       using preamble_type = PreambleT;
       //...
    };


    Такой пример class Buffer, по-моему, скорее плохой, потому что только имитирует на C++ код, написанный на C. Так, мы не храним в классе размер буфера, поэтому нельзя определить операторы копирования. Детали реализации класса "утекают" в пользовательский код.
    Пока мы явно не сформулировали и не поддерживаем инварианты класса. Какое состояние объекта мы можем считать пригодным для использования?
    В частности, мы можем переместить содержимое буфера в другой объект конструктором перемещения, но потом к исходному буферу не можем применить resize. Даже если бы resize давал нам новый буфер, значение preamble потеряно. Если бы мы сохраняли preamble, тогда в конструкторе перемещения пришлось бы выделять память под новый буфер, но тогда этот конструктор уже не будет noexcept - плохо. Придется запретить перемещение?

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

    Наверняка есть лучшее проектное решение, такое которое не будет имитировать реализацию на C. Возможно, имеет смысл портировать решение на уровне интерфейсов, переписывая полностью на C++ детали реализации.
    Ответ написан
    Комментировать
  • Каковы правила конвертации указателя на массив неопределенной длины в указатель на массив определенной длины?

    @code_panik
    В выражении p = p2 мы выполняем неявное преобразование встроенных типов, для которых в стандарте не описаны правила преобразования. То есть такое преобразование не гарантировано, но может быть реализовано компилятором, например
    #include <type_traits>
    using namespace std;
    
    int main() {
        static_assert(std::is_convertible<int(*)[2], int(*)[]>::value, "Non-convertible");
        return 0;
    }

    компилируется в gcc 12.2, в clang 15.0 - ошибка.

    Существует старое не принятое предложение о закреплении такого преобразования стандартом.
    Ответ написан
    1 комментарий
  • Можно ли создать пустую структуру, а потом её заполнить внутри функции?

    @code_panik
    Можно ли создать пустую структуру, а потом её заполнить внутри функции?

    Нет, в языке c++ все типы данных должны быть известны компилятору при компиляции программы.

    Для работы с текстовыми настройками программы есть готовые решения, например qsettings в qt, libconfig.

    Если нужна своя реализация, можно придумать разные варианты интерфейсов для class Config. Если нам нужны отдельные объекты вроде struct Date, можем реализовать фабричный метод, который будет их создавать, как в следующем примере.

    #include <iostream>
    #include <string>
    #include <unordered_map>
    #include <fstream>
    
    class AppConfig {
    public:
        static AppConfig make(const std::string& path) {
    		    auto attributes = readAttributes(path);
    		    if (attributes.empty()) {
    			    // signal error - no attributes, no config
    			    return {};
    		    }
            return parse(attributes);
        }    
    
        std::string title = "";
        bool fullscreen = false;
        bool windowMode = false;
        int width = 0;
        int height = 0;
        int renderApi = 0;
    
    private:    
        static std::unordered_map<std::string, std::string>
        readAttributes(const std::string& path) {
            std::unordered_map<std::string, std::string> attributes;
            std::ifstream configFile(path);
            if (!configFile) {
                // signal error - failed to open file
                return {};
            }
            std::string attribute, value;
            while (configFile >> attribute >> value) // attribute value
                attributes[attribute] = value;
            return attributes;
        }
    
        static AppConfig parse(std::unordered_map<std::string, std::string>& attributes) {
            static constexpr auto trueValue = "true";
            AppConfig config;
            config.title = attributes["title"];
            config.fullscreen = attributes["fullscreen"] == trueValue;
            config.windowMode = attributes["window_mode"] == trueValue;
            config.width = std::stoi(attributes["width"]);
            config.height = std::stoi(attributes["height"]);
            config.renderApi = std::stoi(attributes["render_API"]);
            return config;
        }    
    };
    
    class Window {
    public:
    	static bool create_window(...) { return true; }
    };
    
    bool initialize() {
        auto config = AppConfig::make("settings.txt");
        if (!Window::create_window(config.title, config.fullscreen, config.windowMode, 
            config.width, config.height))
            return false;
        //...
        return true;
    }


    Класс Config может иметь интерфейс словаря, который можно реализовать с помощью std::any. При этом можно убрать промежуточные классы и всегда обращаться непосредственно к Config.

    #include <unordered_map>
    #include <string>
    #include <any>
    
    class AppConfig {
    public:
        explicit AppConfig(const std::string& path) {  /* read values */  }
        std::any operator[](const std::string& attribute) {  return attributes[attribute];  }
    private:    
        std::unordered_map<std::string, std::any> attributes;
    };
    
    class Window {
    public:
      static bool create_window(...) { return true; }
    };
    
    bool initialize() {
        auto config = AppConfig("settings.txt");
        if (!Window::create_window(
            std::any_cast<std::string>(config["title"]),
            std::any_cast<bool>(config["fullscreen"]),
            std::any_cast<bool>(config["window_mode"])
            /* ... */)) {
            return false;
        }
        return true;
    }
    Ответ написан
  • Является ли такой способ выделения массива объектов на хипе идиоматичным?

    @code_panik
    "Если требуется работа с голой памятью", - в c++ обычно не работают с голой памятью (free store). Работают с объектами, у которых есть время жизни. Мы выделяем память посредством низкоуровневых функций, только чтобы потом в ней создавать живые объекты, например с помощью placement new.

    В этом смысле подход "без использования векторов и прочего" не имеет совершенно никакого значения. Как руками мы выделяем память и создаем в ней объекты, так поступает и вектор. Но только вектор позволяет нам сохранить здоровыми несколько клеток головного мозга. Другие аргументы в соответствующем разделе super-faq по ключевому слову vector.

    "например, какое-то апи требует тип T *" - vector<T>::data().

    Идиоматичность кода подразумевает использование актуальных стандартных средств языка. Оба примера напоминают c++03. Если код плохо читаем как в первом примере, то имеет смысл поискать лучшие альтернативы. Если их нет, то завернуть код во что-нибудь удобочитаемое, вектор например. Про идиоматичную низкоуровневую работу с памятью можно почитать у Страуструпа в примере реализации вектора.

    Вопрос оптимизации - отдельная тема. Оптимизация под конкретные нужды не обязана быть идиоматичной или в принципе соответствовать общим представлениям. Например, вместо хранения массива структур, мы можем создать и использовать отдельно массивы полей структур. Можете поискать информацию по data-oriented design.
    Ответ написан
    Комментировать
  • Как превратить void() в void (**)()?

    @code_panik
    UPD: Похоже, проблема в библиотеке
    https://github.com/espressif/arduino-esp32/issues/7675
    Исправляющий комит
    https://github.com/espressif/arduino-esp32/commit/...

    Довольно странное требование для api.
    Если вы пишите класс-обертку над этой функцией, то она принимает просто функцию, а не указатель на указатель.
    В C++ функция определяется своим адресом, и правила передачи функции в функцию похожи на правила передачи обычного массива.
    Похоже, проблема именно в объявлении вашего register_callback, которое можно реализовать как в этом примере
    #include <iostream>
    using namespace std;
    
    using esp_spp_cb_event_t = int;
    using esp_spp_cb_param_t = void;
    
    void btCallback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
        cout << "btCallback" << endl;
    }
    
    class MyBt {        
    public:
        typedef void (*callback_type)(esp_spp_cb_event_t, esp_spp_cb_param_t *);    
    
        void register_callback(callback_type cb) {
            cb(0, nullptr);
        }
    };
    
    int main()
    {    
        MyBt bt;
        bt.register_callback(btCallback);
        return 0;
    }
    Ответ написан
    5 комментариев
  • Как в деструкторе базового класса вызвать переопределённый метод?

    @code_panik
    Порядок вызова деструкторов определяется стандартом языка. Сначала выполняется деструктор дочернего класса, затем деструкторы членов дочернего класса и только потом родительские деструкторы (https://isocpp.org/wiki/faq/dtors#order-dtors-for-...). Поэтому доступные данные и методы в родительском деструкторе ограничены родительским подобъектом и его родителями.
    Родительский деструктор можно сделать тривиальным ~Parent() = default (ничего не делать) или чистым виртуальным virtual ~Parent() = 0 (не определен). Только во втором случае невозможно создавать объекты типа "Parent".
    Ответ написан
    Комментировать
  • Почему msvc оптимизирует конструкторы несмотря на флаги?

    @code_panik
    Разберемся, в каком порядке выводятся сообщения на экран во втором случае gnu++11.
    Сначала в main вызываются два конструктора по умолчанию: для imnotdumb1 и временного объекта dumb_array();.
    Далее в main вызывается метод dumb_array & operator=( dumb_array temp), в котором temp инициализируется посредством метода dumb_array (dumb_array&& other), который перед выводом на экран сообщения operator=( dumb_array tmp) вызывает ещё один конструктор по умолчанию.
    Итого 3 конструктора по умолчанию, один перемещения, один оператор присваивания.

    MSVC вызывает первые два конструктора по умолчанию и применяет move elision к инициализации temp.
    То есть весь пример сводится к такому коду
    #include <iostream>
    using namespace std;
    
    struct Foo {
    	Foo() { cout << "default ctor\n"; }
    	Foo(Foo&& arg) { cout << "move ctor\n"; }
    };
    
    void foo(Foo arg) {
    	cout << "foo\n";
    }
    
    int main () {    
    	foo(Foo());
    	return 0;
    }


    В c++ существуют обязательные (mandatory) и необязательные случаи copy/move elision, https://en.cppreference.com/w/cpp/language/copy_elision.
    Применение правила из примера
    In the initialization of an object, when the source object is a nameless temporary and is of the same class type (ignoring cv-qualification) as the target object.

    до c++17 было необязательным к реализации, на усмотрение разработчиков компилятора.
    В Microsoft считают такое правило обязательным вне зависимости от флагов компиляции,
    Mandatory copy/move elision in Visual Studio

    The C++ standard requires copy or move elision when the returned value is initialized as part of the return statement (such as when a function with return type Foo returns return Foo()). The Microsoft Visual C++ compiler always performs copy and move elision for return statements where it is required to do so, regardless of the flags passed to the compiler. This behavior is unchanged.


    До c++17 требовалось, чтобы соответствующий copy/move ctor в случае copy elision всё равно был доступен (cppreference, там же):
    This is an optimization: even when it takes place and the copy/move (since C++11) constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed
    Ответ написан
    1 комментарий
  • Почему нет доступа к приватному атрибуту?

    @code_panik
    Пожалуйста, приводите в вопросах содержимое файлов с кодом полностью. Без полных листингов нельзя точно сказать, в чем проблема.
    Обратите внимание, что для вызова operator+(Car& c1, Car& c2) функция GetCar должна возвращать Car&, а не Car или const Car& (только не возвращайте ссылку на переменную, объявленную внутри функции).
    Замените везде operator+(Car& c1, Car& c2) на operator+(const Car& c1, const Car& c2).
    Работающий пример.
    И замените, по возможности, компилятор на более современный.
    Ответ написан
  • Как убрать ошибки взаимодействия между классами?

    @code_panik
    Если структуру классов не нужно менять, то можно сделать как в рабочем примере.
    Перед Window_mgr достаточно объявить (forward declare) class Screen. Просто нужно иметь ввиду, что в файле класса Window_mgr это неполный тип (incomplete type).
    И во friend объявлении не хватает Window_mgr::ScreenIndex.
    Ответ написан
    1 комментарий
  • Как это можно реализовать?

    @code_panik
    Если enum не видно извне как деталь реализации, то можно так.
    #include <iostream>
    using namespace std;
    
    class BaseTransport {    
    public:
        virtual void Move() {        
            onArrival();            
        }
    private:
        virtual void onArrival() = 0;
    };
    
    class GroundTransport : public BaseTransport {
    private:
        void onArrival() override {
            cout << __PRETTY_FUNCTION__ << endl;
            state = GroundEnum::Finished;
        }
        enum class GroundEnum { Finished };    
        GroundEnum state;
    };
    
    class SeaTransport : public BaseTransport {    
    private:
        void onArrival() override {
            cout << __PRETTY_FUNCTION__ << endl;
            state = SeaEnum::ReturnToBegin;
        }
        enum class SeaEnum { ReturnToBegin };
        SeaEnum state;
    };
    
    int main() {
        BaseTransport* st = new SeaTransport();
        st->Move();    
        BaseTransport* gt = new GroundTransport();
        gt->Move();
        return 0;
    }
    Ответ написан
    Комментировать