При написании одного клиент-серверного приложения на С++ потребовалось организовать защищённое соединение между двумя удалёнными узлами. Я сразу обратил внимание на алгоритмы использующиеся в TLS 1.3. Опустив реализацию TLS-сертификатов и взяв только часть ответственную за шифрования данных, я приступил к работе. Информацию пришлось выискивать в во множестве совершенно разных источниках(от официальной документации OpenSSL, до ответов на stackoverflow), а местами даже додумывать каким образом скрепить куски кода из этих самых разных источников. Так что после успешной реализации задуманного, я решил написать данную статью, с целью помочь тем кто будет решать подобную проблему. В данной статье мы рассмотрим простую реализацию связки алгоритма согласования ключей Диффи-Хеллман на элиптических кривых и алгоритма симметричного шифрования AES 256 с использованием библиотеки OpenSSL для организации защищённого соединения.

Генерация ключей ECDH

Заголовочный файл выглядит следующим образом:

#ifndef SECURITY_HPP
#define SECURITY_HPP

#include <openssl/ecdh.h>
#include "byte_array.hpp"

namespace security {

/**
 * @brief AES256 key and initialization vector
 */
struct AES_t {
  uint8_t key[32] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0
  };
  uint8_t init_vector[16] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0
  };
  bool isEmpty() const {
      static const AES_t empty_aes;
      return !std::memcmp(this, &empty_aes, sizeof (AES_t));
  }
  void clear() {*this = AES_t();}
  AES_t() = default;
  AES_t(ByteArray data) {
    *this = *reinterpret_cast<AES_t*>(data.begin());
  }
};

// ECDH
/**
 * @brief Generate ECDH key pair
 * @return ECDH key pair
 */
EVP_PKEY* genKey();

/**
 * @brief Free key memory
 * @param key
 */
void freeKey(EVP_PKEY* key);

/**
 * @brief Extract public key from key pair
 * @param key_pair
 * @return ByteArray with public key
 */
ByteArray extractPublicKey(EVP_PKEY* key_pair);

/**
 * @brief Extract private key from key pair
 * @param key_pair
 * @return ByteArray with private key
 */
ByteArray extractPrivateKey(EVP_PKEY* key_pair);

/**
 * @brief Conver ByteArrays to key pair
 * @param priv_key_raw
 * @param pub_key_raw
 * @return ECDH key pair
 */
EVP_PKEY* getKeyPair(ByteArray priv_key_raw, ByteArray pub_key_raw);

/**
 * @brief Get AES256 key and initialization vector from ECDH keys
 * @param peer_key - public key from other side
 * @param key_pair - private key from this side
 * @return AES256 key and initialization vector
 */
AES_t getSecret(ByteArray peer_key, EVP_PKEY* key_pair);

// AES256
/**
 * @brief Encrypt message
 * @param plain_text
 * @param aes_struct
 * @return Cyphertext
 */
ByteArray encrypt(ByteArray plain_text, AES_t aes_struct);

/**
 * @brief Decrypt message
 * @param ciphertext
 * @param aes_struct
 * @return plain_text
 */
ByteArray decrypt(ByteArray ciphertext, AES_t aes_struct);

/**
* @brief Encode data with base64
* @param decoded
* @return
*/
ByteArray encodeBase64(ByteArray decoded);

/**
* @brief Decode base64 data
* @param encoded
* @return
*/
ByteArray decodeBase64(ByteArray encoded);

}

#endif // SECURITY_HPP

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

// файл security.cpp
#include "security.hpp"

#include <openssl/conf.h>
#include <openssl/err.h>
// EVP
#include <openssl/evp.h>
// AES
#include <openssl/aes.h>
// ECDH
#include <openssl/ec.h>
#include <openssl/pem.h>

#include <stdexcept>

// Обработка ошибок
void handleErrors() {
  ERR_print_errors_fp(stderr);
  throw std::runtime_error("Security error");
}

// Нижеприведённый код здесь

Для начала каждая из сторон должна сгенрировать пары ключей ECDH. Для работы с ключами ECDH будем использовать высокоуровневый интерфейс OpenSSL - EVP.

EVP_PKEY* security::genKey() {
  EVP_PKEY* key_pair = nullptr;						// Ключевая пара
  EVP_PKEY_CTX* param_gen_ctx = nullptr; 	// Контекст генерации параметров
  EVP_PKEY_CTX* key_gen_ctx = nullptr;		// Контекст генерации ключа
  EVP_PKEY* params= nullptr;							// Параметры ключа

  // Выделяем память для контекста генерации параметров EC-ключа
  if(!(param_gen_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL))) handleErrors();
  // Инициализируем контекст генерации параметров EC-ключа
  if(!EVP_PKEY_paramgen_init(param_gen_ctx)) handleErrors();

  // Задаём элиптичекую кривую prime256v1
  if(!EVP_PKEY_CTX_set_ec_paramgen_curve_nid(param_gen_ctx, NID_X9_62_prime256v1))
    handleErrors();

  // Генерируем параметры
  if(!EVP_PKEY_paramgen(param_gen_ctx, &params)) handleErrors();

  // Выделяем память для контекста генерации EC-ключа
  if(!(key_gen_ctx = EVP_PKEY_CTX_new(params, nullptr))) handleErrors();
  // Инициализируем контекст генерации EC-ключа
  if(!EVP_PKEY_keygen_init(key_gen_ctx)) handleErrors();
  // Генерируем ключ
  if(!EVP_PKEY_keygen(key_gen_ctx, &key_pair)) handleErrors();

  // Высвобождаем память контекста генерации параметров
  EVP_PKEY_CTX_free(param_gen_ctx);
  // Высвобождаем память контекста генерации ключа
  EVP_PKEY_CTX_free(key_gen_ctx);
  // Возвращаем указатель ключевой пары
  return key_pair;
}

Извлечение ключей ECDH из ключевой пары EVP_PKEY* в "сырой буффер"

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

Поскольку работаем мы с C++ для хранения сырых данных можно использовать std::vector<uint8_t> или класс на подобии нижеописанного:

// Файл byte_array.hpp
#ifndef BYTE_ARRAY_HPP
#define BYTE_ARRAY_HPP

#include <cstdint>
#include <cstring>
#include <utility>
#include <new>
#include <malloc.h>

class ByteArray {
  uint8_t* byte_array = nullptr;
  uint64_t _length = 0;
  public:
  typedef uint8_t* iterator;
  // Конструктор по умолчанию
  ByteArray() = default;
  
  // Коснтруктор с выделением памяти
  ByteArray(uint64_t length)
    : byte_array(new uint8_t[length]),
  		_length(length) {}
  
  // Конструктор копирования из сырого буфера
  ByteArray(void* buffer, uint64_t length)
    : byte_array(new uint8_t[length]),
      _length(length) {
        memcpy(byte_array, buffer, _length);
      }
  
  // Конструктор копирования
  ByteArray(ByteArray& other)
    : byte_array(new uint8_t[other._length]),
      _length(other._length) {
        memcpy(byte_array, other.byte_array, _length);
      }
  
  // Конструктор перемещения
  ByteArray(ByteArray&& other)
    : byte_array(other.byte_array),
      _length(other._length) {
        other.byte_array = nullptr;
      }
  
  // Деструктор
  ~ByteArray() {if(byte_array) delete[] byte_array;}
  
  // Изменить размер
  void resize(uint64_t new_length) {
  	_length = new_length;
  	byte_array = (uint8_t*)realloc(byte_array, _length);
  }
  
  // Добавить размер
  iterator addSize(uint64_t add) {
    byte_array = (uint8_t*)realloc(byte_array, _length + add);
    iterator it = byte_array + _length;
    _length += add;
    memset(it, 0, add);
    return it;
  }
  
  // Getter для размера
  inline uint64_t length() {return _length;}
  // Оператор взятие елемента
  inline uint8_t& operator[](uint64_t index) {return byte_array[index];}
  // Оператор присвоения
  inline ByteArray& operator=(ByteArray other) {
    this->~ByteArray();
    return *new(this) ByteArray(std::move(other));
  }
  
  // Итераторы для range-based for
  // for(auto byte : byte_array_object) {...}
  inline iterator begin() {return byte_array;}
  inline iterator end() {return byte_array + _length;}
};

#endif // BYTE_ARRAY_HPP

Функция извлечения публичного ключа:

ByteArray security::extractPublicKey(EVP_PKEY* key_pair) {
  EC_KEY* ec_key = EVP_PKEY_get1_EC_KEY(key_pair);
  EC_POINT* ec_point = const_cast<EC_POINT*>(EC_KEY_get0_public_key(ec_key));

  EVP_PKEY* public_key = EVP_PKEY_new();
  EC_KEY* public_ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);

  EC_KEY_set_public_key(public_ec_key, ec_point);
  EVP_PKEY_set1_EC_KEY(public_key, public_ec_key);


  EC_KEY *temp_ec_key = EVP_PKEY_get0_EC_KEY(public_key);

  if(temp_ec_key == NULL) handleErrors();

  const EC_GROUP* group = EC_KEY_get0_group(temp_ec_key);
  point_conversion_form_t form = EC_GROUP_get_point_conversion_form(group);

  unsigned char* pub_key_buffer;
  size_t length = EC_KEY_key2buf(temp_ec_key, form, &pub_key_buffer, NULL);
  if(!length) handleErrors();
  ByteArray data(pub_key_buffer, length);

  OPENSSL_free(pub_key_buffer);
  EVP_PKEY_free(public_key);
  EC_KEY_free(ec_key);
  EC_KEY_free(public_ec_key);
  EC_POINT_free(ec_point);

  return data;
}

Функция извлечения приватного ключа:

ByteArray security::extractPrivateKey(EVP_PKEY* key_pair) {
  EC_KEY* ec_key = EVP_PKEY_get1_EC_KEY(key_pair);
  const BIGNUM* ec_priv = EC_KEY_get0_private_key(ec_key);
  int length = BN_bn2mpi(ec_priv, nullptr);
  ByteArray data(length);
  BN_bn2mpi(ec_priv, data.begin());
  return data;
}

Для получения пары ключей EVP из двух "сырых буферов" можно воспользоваться следующей функцией:

EVP_PKEY* security::getKeyPair(ByteArray priv_key_raw, ByteArray pub_key_raw) {
  EVP_PKEY* key_pair = EVP_PKEY_new();
  EC_KEY *ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);

  const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key);
  EC_POINT* ec_point = EC_POINT_new(ec_group);
  EC_POINT_oct2point(ec_group, ec_point, pub_key_raw.begin(), pub_key_raw.length(), nullptr);
  EC_KEY_set_public_key(ec_key, ec_point);
  EC_POINT_free(ec_point);

  BIGNUM* priv = BN_mpi2bn(priv_key_raw.begin(), priv_key_raw.length(), nullptr);
  EC_KEY_set_private_key(ec_key, priv);
  BN_free(priv);

  EVP_PKEY_set1_EC_KEY(key_pair, ec_key);
  EC_KEY_free(ec_key);
  return key_pair;
}

Для хранения или передачи ключей не в бинарном, а в текстовом формате можно закодировать буффер с помощью кодировки base64 которая так же поддерживается в OpenSSL:

// Закодировать в base64
ByteArray security::encodeBase64(ByteArray decoded) {
  ByteArray encoded((4*((decoded.length()+2)/3)) + 1);
  EVP_EncodeBlock(encoded.begin(), decoded.begin(), decoded.length());
  return encoded;
}

// Декодировать из base64
ByteArray security::decodeBase64(ByteArray encoded) {
  ByteArray decoded((3*encoded.length()/4) + 1);
  size_t recived_data_size = EVP_DecodeBlock(decoded.begin(), encoded.begin(), encoded.length());
  if(recived_data_size < decoded.length())
    decoded.resize(recived_data_size);
  return decoded;
}

Получение общего секрета через протокол ECDH

Каждая из сторон сгенерировала по ключевой паре и успешно обменялась своими публичными ключами. Теперь каждая из сторон должна получить "общий секрет" хэш от которого будет использоваться как ключ AES 256. Для начала определим структуру ключа AES 256:

struct AES_t {
  // 32 байта для ключа
  uint8_t key[32] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0
  };
  // 16 байт для вектора инициализации
  uint8_t init_vector[16] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0
  };
  
  // В общей сумме размер ключа 48 байт или 384 бита
  
  // Проверка на пустоту ключа
  bool isEmpty() const {
    static const AES_t empty_aes;
    return !std::memcmp(this, &empty_aes, sizeof (AES_t));
  }
  // Очистить ключ
  void clear() {*this = AES_t();}
  // Конструктор по умолчанию
  AES_t() = default;
  // Конструктор из ByteArray
  AES_t(ByteArray data) {
    *this = *reinterpret_cast<AES_t*>(data.begin());
  }
}

Как мы видим структура для AES 256 ключа занимает 48 байт или 384 бита, а следовательно для получения AES 256 ключа из общего секрета подойдёт хэш-дайджест размером в 384 бит, то есть нам подходят такие алгоритмы хэширования как sha384(sha2) и sha3_384(ранее известен как Keccak). Не стоит воспринимать алгоритм sha3 как приемника алгоритма sha2, на текущий момент принято считать что оба алгоритма достаточно безопасны для использования, что не сказать про sha1. Оба этих алгоритма поддерживаются OpenSSL, но в данном случае всё-таки воспользуемся алгоритмом sha3_384.

AES_t security::getSecret(ByteArray peer_key, EVP_PKEY* key_pair) {
  EC_KEY *temp_ec_key = nullptr;
  EVP_PKEY *peerkey = nullptr;

  // Извлекаем полученный с другой стороны публичный ключ
  // из сырого буффера в EVP_PKEY*
  temp_ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
  if(temp_ec_key == nullptr)
    handleErrors();
  if(EC_KEY_oct2key(temp_ec_key, peer_key.begin(), peer_key.length(), NULL) != 1)
    handleErrors();
  if(EC_KEY_check_key(temp_ec_key) != 1) handleErrors();
  peerkey = EVP_PKEY_new();
  if(peerkey == NULL)
    handleErrors();
  if(EVP_PKEY_assign_EC_KEY(peerkey, temp_ec_key)!= 1)
    handleErrors();

  // Получение общего секрета
  EVP_PKEY_CTX *derivation_ctx = EVP_PKEY_CTX_new(key_pair, NULL);
  EVP_PKEY_derive_init(derivation_ctx);
  EVP_PKEY_derive_set_peer(derivation_ctx, peerkey);
  size_t lenght;	// Размер общего секрета
  void* ptr;			// Указатель на буффер с общим секретом
  if(1 != EVP_PKEY_derive(derivation_ctx, NULL, &lenght)) handleErrors();
  if(NULL == (ptr = OPENSSL_malloc(lenght))) handleErrors();
  if(1 != (EVP_PKEY_derive(derivation_ctx, (unsigned char*)ptr, &lenght))) handleErrors();
  EVP_PKEY_CTX_free(derivation_ctx);
  EVP_PKEY_free(peerkey);

  // Хэшируем общий секрет и записываем в структуру AES_t
  AES_t aes_key;
  EVP_MD_CTX *mdctx;
  if((mdctx = EVP_MD_CTX_new()) == NULL)
    handleErrors();
  if(1 != EVP_DigestInit_ex(mdctx, EVP_sha384(), NULL))
    handleErrors();
  if(1 != EVP_DigestUpdate(mdctx, ptr, lenght))
    handleErrors();
  unsigned int length;
  if(1 != EVP_DigestFinal_ex(mdctx, (unsigned char*)&aes_key, &length))
    handleErrors();
  EVP_MD_CTX_free(mdctx);
  OPENSSL_free(ptr);
  return aes_key;
}

Шифрование и дешифрование с использованием AES 256

На текущий момент обе стороны имеют согласованный ключ AES 256 и теперь можно приступить непосредственно к шифрованию данных:

ByteArray security::encrypt(ByteArray plain_text, AES_t aes_struct) {
  // Рассчитываем длинну шифротекста
  ByteArray ciphertext(plain_text.length() % AES_BLOCK_SIZE == 0
                       ? plain_text.length()
                       : (plain_text.length() / AES_BLOCK_SIZE + 1) * AES_BLOCK_SIZE);

  // Инициализация контекста шифра
  EVP_CIPHER_CTX *ctx;
  if(!(ctx = EVP_CIPHER_CTX_new()))
    handleErrors();
  if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, aes_struct.key, aes_struct.init_vector))
    handleErrors();

  // Шифрование исходного текста
  int f_length, s_length;
  if(1 != EVP_EncryptUpdate(ctx, ciphertext.begin(), &f_length, plain_text.begin(), plain_text.length()))
    handleErrors();

  // Иногда для записи шифротекста требутеся дополнительный AES блок
  if(uint64_t(f_length) == ciphertext.length())
    ciphertext.addSize(AES_BLOCK_SIZE);
  else if(uint64_t(f_length) > ciphertext.length())
    throw std::runtime_error("Predicted ciphertext size lower then actual!");

  // Запись последнего AES блока
  if(1 != EVP_EncryptFinal_ex(ctx, ciphertext.begin() + f_length, &s_length))
    handleErrors();
  
  // Уменьшение размера данных до размера записанного шифротекста
  if(uint64_t reuired_length = f_length + s_length; reuired_length < ciphertext.length())
    ciphertext.resize(f_length + s_length);
  else if(reuired_length > ciphertext.length())
    throw std::runtime_error("Predicted ciphertext size lower then actual!");

  // Высвобождения выделенной памяти для контекста шифра
  EVP_CIPHER_CTX_free(ctx);

  return ciphertext;
}

Дешифровка данных выглядит следующим образом:

ByteArray security::decrypt(ByteArray ciphertext, AES_t aes_struct) {
  ByteArray plain_text(ciphertext.length());

  // Инициализация контекста шифра
  EVP_CIPHER_CTX *ctx;
  if(!(ctx = EVP_CIPHER_CTX_new()))
    handleErrors();
  if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, aes_struct.key, aes_struct.init_vector))
    handleErrors();

  // Дешифровка шифротекста
  int f_length, s_length;
  if(1 != EVP_DecryptUpdate(ctx, plain_text.begin(), &f_length, ciphertext.begin(), ciphertext.length()))
    handleErrors();
  if(1 != EVP_DecryptFinal_ex(ctx, plain_text.begin() + f_length, &s_length))
    handleErrors();
  
  // Уменьшение размера буффера до размера полученных данных
  plain_text.resize(f_length + s_length);

  // Высвобождения выделенной памяти для контекста шифра
  EVP_CIPHER_CTX_free(ctx);

  return plain_text;
}

Пример использования

Ниже приведён простой пример использования вышеописанного кода:

#include "security.hpp"
#include <iostream>

int main(int argc, char* argv[]) {
  using namespace security;
  // Алиса генерирует ключ
  EVP_PKEY* alice_key_pair = genKey();
  // Алиса извлекает публичный ключ
  ByteArray alice_peer_key = extractPublicKey(alice_key_pair);
  
  // Боб генерирует ключ
  EVP_PKEY* bob_key_pair = genKey();
  // Боб извлекает публичный ключ
  ByteArray bob_peer_key = extractPublicKey(bob_key_pair);
  
  // Алиса и Боб обмениваются публичными ключами
  // через открытый канал передачи данных
  
  // Боб получает согласованный AES 256 ключ
  AES_t bob_aes_key = getSecret(alice_peer_key, bob_key_pair);
  
  // Алиса получает согласованный AES 256 ключ
  AES_t alice_aes_key = getSecret(bob_peer_key, alice_key_pair);
  
  // Алиса шифрует сообщение
  std::string alice_msg = "Hello, Bob";
  ByteArray alice_msg_buffer(alice_msg.data(), alice_msg.length() + 1);
  ByteArray alice_enc_msg = encrypt(alice_msg_buffer, alice_aes_key);
  // И передаёт зашифрованное сообщение по открытому каналу Бобу
  
  // Боб шифрует сообщение
  std::string bob_msg = "Hello, Alice";
  ByteArray bob_msg_buffer(bob_msg.data(), bob_msg.length() + 1);
  ByteArray bob_enc_msg = encrypt(bob_msg_buffer, bob_aes_key);
  // И передаёт зашифрованное сообщение по открытому каналу Алисе
  
  // Алиса получает и дешифровывет сообщение
  ByteArray alice_recived_msg = decrypt(bob_enc_msg, alice_aes_key);
  std::cout << "Bob: " << (char*)alice_recived_msg.begin() << '\n';
  
  // Боб получает и дешифровывет сообщение
  ByteArray bob_recived_msg = decrypt(alice_enc_msg, bob_aes_key);
  std::cout << "Alice: " << (char*)bob_recived_msg.begin() << '\n';

  return 0;
}

Исходный код представлен в этом репозитории GitHub.

Комментарии (9)


  1. Inobelar
    24.11.2021 03:47
    +7

    Не хочу прослыть токсиком, но ... у Вас просто C++ wrapper, никакого отношения отношения к C++17 и даже C++11. Заголовок вводит в заблуждение.

    "Доставили" следующие фрагменты кода:

    void AES_t::clear() { *this = AES_t(); }
    
    // Оператор присвоения   
    inline ByteArray& operator=(ByteArray other) {
        this->~ByteArray();     
        return *new(this) ByteArray(std::move(other));
    }

    Надеюсь никогда такого не увидеть в production :D


    1. old2ev Автор
      24.11.2021 08:48
      -1

      Из-за наличия инициализации в условных конструкциях:

      if(uint64_t reuired_length = f_length + s_length; reuired_length < ciphertext.length())
          ciphertext.resize(f_length + s_length);
        else if(reuired_length > ciphertext.length())
          throw std::runtime_error("Predicted ciphertext size lower then actual!");

      Которая появилась в C++ 17-й версии, есть требования к версии C++ при компиляции кода, из-за этого и появилась версия C++ в названии, поскольку в некоторых версиях MinGW попрежнему явно приходится задавать версию при компиляции, если требуется поддержка версии C++ выше 14-й, я решил на это указать.

      Хотелось бы уточнить, что именно не устраивает в участках кода, что вы отметили:

      // Присвоить к текущему экземпляру класса AES_t
      // экземпляр AES_t сконструированный поумолчанию
      void AES_t::clear() { *this = AES_t(); }
      // Использование логики определённой в RAII для копирования объекта
      // при присвоении, по сути вызвать деструктор и конструктор через placement-new от
      // скопированного/перемещённого из вне функции экземпляра данного
      // класса
      inline ByteArray& operator=(ByteArray other) {
          this->~ByteArray();     
          return *new(this) ByteArray(std::move(other));
      }

      Вполне понятные и рабочие варианты сделать то что от данных методов требуется.

      По поводу того, что это просто обёртка для работы с функциями OpenSSL - вы правы, стоило это указать в названии статьи, чтобы небыло заблуждений по поводу содержания. Уже исправил.


      1. Ingulf
        24.11.2021 10:15
        -1

        1. конструирование нового объекта как способ очистить старый - немного странно выглядит, это мягко говоря, я даже удивлен, что это вообще как-то по-видимому работает

        2. Зачем выделять объект через new? тем более мувить в него переданный по значению (то есть скопированный до этого в аргумент функции) объект?? А тем временем это не похоже не на копирующий, ни на перемещающий операторы присваивания. Обычно либо копируются мемберы или перемещаются они же, а тут что-то очень странное происходит, а потом будете говорить, что на С++ писать не возможно))
          Ручной вызов деструктора для this? Вы разрушаете объект во время действия его метода? выглядит странно. Деструктор автоматов выховется для уничтожаемого объекта.
          И, наконец, что будет с выделенной памятью если, например 3 раза подряд вызвать этот оператор? ну для трех объектов память мы выделим, а кто будет освобождать?


        1. Chuvi
          24.11.2021 10:25
          +2

          По поводу 3-го. Ничего не будет, так как используется placement new. https://en.cppreference.com/w/cpp/language/new Он не выделяет память, а конструирует объект в указанном, заранее выделенном месте.


          1. Ingulf
            03.12.2021 15:19

            а вот то, что это placement new - я упустил, да


        1. old2ev Автор
          24.11.2021 10:53

          Да, выглядит странно, но тем не менее это рабочий способ описать лаконично некоторые методы. Сам по себе деструктор не вычищает память из под экземпляра класса это делает только delete. По факту оператор delete - это комбинация вызовов деструкторов и последующий вызов free. Деструктор служит лишь для деинициализации экземпляра класса. Сделано это так чтобы можно было как переместить объект, так и скопировать:

          ByteArray a(15), b(10), c(12);
          b = a; // Скопировали a в b
          // Вызванный деструктор b очистит выделенную память в указателе b.byte_array
          c = std::move(a); // Переместили a в c
          // Вызванный деструктор c очистит выделенную память в указателе c.byte_array

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

          ByteArray& ByteArray::operator=(ByteArray other) {
          	if(byte_array) delete byte_array;
            _length = other._length;
            byte_array = other.byte_array;
            other.byte_array = nullptr;
            return *this;
          }

          Но если разницы особой нет, особого смысла это не имеет. Иногда случается, что в классе множество разных поинтеров, память из которых нужно вычищать. Да, можно вынести код очистки в отдельный приватный метод например в void clear() и за тем уже вызывать их в деструкторах и операторах присвоения. Можно конечно ещё использовать std::unique_ptr.

          Что касательно использования placement-new - то он нужен лишь за тем чтобы вызвать конструктор у куска не инициализированной памяти, тоже вполне стандартная функция не взывающая никаких аллокаций/реаллокаций памяти кроме тех что прописаны в конструктор непосредственно.

          Например им удобно пользоваться, если в некотором куске не инициализированной памяти надо сконструировать два разных экземпляра:

          uint8_t* a = (uint8_t*)malloc(sizeof(uint32_t) + sizeof(long double));
          new(a) uint32_t(255);
          new(a+sizeof(uint32_t)) long double(15.256);


        1. Tujh
          24.11.2021 13:43

          конструирование нового объекта как способ очистить старый - немного странно выглядит, это мягко говоря, я даже удивлен, что это вообще как-то по-видимому работает

          Вы не работали с embedded, где порой вообще не бывает реализации new и malloc, так как не существует "кучи" как таковой, это, конечно про глубокий bare-metal embedded, но всё же.


          1. INSTE
            24.11.2021 17:35

            Код «попахивает», и никакой необходимости в таком подходе здесь нет.


            1. Tujh
              24.11.2021 18:45

              никакой необходимости в таком подходе здесь нет.

              это отдельный вопрос, я только объяснил трюк с ручным вызовом деструктора и placement new для конструирования нового объекта. Для обычных приложений с полноценной ОС он нужен крайне редко, для встройки применяется очень часто.

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