C : инициализировать переменную определенного типа, logic. Consise способ решения этих проблем

#c

#c

Вопрос:

У меня вообще есть эта проблема, но мой конкретный пример: при работе с .данные wav, для 16-битных волн используется целое число со знаком, тогда как 8-битные волны без знака.

Я хотел бы сделать что-то вроде следующего:

 if (bytesPerSample == 2){
      int16_t* buffer = new int16_t[1];
} else if (bytesPerSample == 1){
      uint8_t* buffer = new uint8_t[1];
} else {
      cout << "bytesPerSample: " << bytesPerSample << " is unsupported"  << endl;
}
 

Я знаю, что это недопустимо, потому что переменная, объявленная внутри области действия оператора if, уничтожается при выходе из блока.

Я также знаю, что мог бы инициализировать оба, а затем я мог бы создать 2 оператора if и скопировать остальную часть моего кода дважды, изменить имена переменных, а затем выполнить один блок, только если #bytes равно 2, а другой, если #bytes равно 2. Однако этот метод не является кратким и может привести к тому, что я введупроблемы при изменении второго блока кода.

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

Комментарии:

1. Рассматривали ли вы шаблоны?

2. С разными типами буфера вы обречены на различия в коде на всем протяжении. У меня, вероятно, была бы шаблонная функция и что-то вроде if (bytesPerSample == 2){ do_stuff<int16_t>(); } else if (bytesPerSample == 1){ do_stuff<uint8_t>();} else...

3. Да, вы можете использовать шаблоны. В качестве альтернативы вы можете использовать объединения. Другой подход, который довольно распространен для аудио, заключается в чтении всего буфера как символа с правильным выравниванием, выполнении любых перестановок для исправления бесконечности, а затем переинтерпретации буфера к правильному типу.

4. union { int16_t* i16; uint8_t* u8; } buffer; будет храниться один указатель, который во время выполнения вы инициализируете на основе базового типа данных, и который затем вы используете только с инициализированным вами членом (в соответствии с правилами использования объединения) — обратитесь к документации . Поскольку вы всегда знаете тип данных, потому что это обязательное условие для выполнения чего-либо, это не должно быть проблемой. Это удобный способ хранить ваши данные в одном месте. Помимо этого, разумной идеей было бы использовать шаблоны функций для общих операций.

5. Оберните его в класс и используйте полиморфизм. Начните с чисто virtual интерфейса, который определяет общие, независимые от размера выборки операции над буфером, но не владеет физическим буфером и не может быть создан. Затем создайте две реализации, которые фактически владеют буферами, с соответствующими перегрузками методов для их размеров выборки. В идеале, две реализации должны (и, вероятно, должны ) быть экземплярами общего шаблона для максимального повторного использования кода, с размером / типом выборки в качестве параметров шаблона. Или, может быть, используйте std::variant intsead, который является дружественным к C union .

Ответ №1:

Тип C 17 std::variant может содержать несколько типов данных в одном пространстве. Затем вы можете использовать std::visit для отправки на основе типа внутри.

Если вы объедините это с шаблонами, вам нужно будет написать только одну версию каждой функции, которая обрабатывает переменную.

 template <typename T>
void processSamples(const std::vector<T>amp; buffer) { 
  /* do your processing here */ 
}

std::variant<
  std::vector<uint8_t>,
  std::vector<uint16_t>> buffer;

if (bytesPerSample == 2) {
  buffer = std::vector<uint16_t>(numSamples);
} else if (bytesPerSample == 1) {
  buffer = std::vector<uint8_t>(numSamples);
}

std::visit([](const autoamp; buffer) { processSamples(buffer); }, buffer);
 

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

 void processSamples(uint8_t*);
void processSamples(uint16_t*);

struct Buffer {
  int bytesPerSample;
  union {
    uint8_t* u8;
    uin16_t* u16;
  }
}

Buffer buffer;
buffer.bytesPerSample = bytesPerSample;

if (buffer.bytesPerSample == 2) {
  buffer.u16 = new uint16_t[16000];
} else if (buffer.bytesPerSample == 1) {
  buffer.u8 = new uint8_t[16000];
}

if (buffer.bytesPerSample == 2) {
  processSamples(buffer.u16);
} else if (buffer.bytesPerSample == 1) {
  processSamples(buffer.u8);
}
 

… за исключением случаев, когда вы используете std::variant , все типобезопасно, принимает нетривиальные типы, и вам не нужно проверять каждый блок переключения на исчерпываемость при внесении изменений.

Если у вас нет C 17, существуют реализации, совместимые с C 11:

https://github.com/mpark/variant