TGViewer
Channel Public Channel
C/C++ | Вопросы собесов

C/C++ | Вопросы собесов

@easy_c_plus

Сайт: https://easyoffer.ru/
Все каналы: t.me/+xGeAw6ckJ4liYzQy

Контакт для рекламы: @sendme_ads
Subscribers
4.19K
Photos
36
Videos
1
Links
1.6K

Showing posts older than #2315 · Back to latest

Older Posts 20 shown
Post #2313 538
🤔 На какой структуре данных построена priority_queue?

priority_queue в стандартной библиотеке построена на бинарной куче (binary heap).
1. Это деревооподобная структура данных, поддерживающая быструю вставку и извлечение максимального или минимального элемента.
2. Вставка и удаление имеют сложность O(log n), а доступ к верхнему элементу — O(1).


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2311 508
🤔 Строгая гарантия безопасности

Гарантии безопасности исключений в C++ делятся на три уровня:
Базовая гарантия (Basic Guarantee) – программа не падает, но объект может остаться в некорректном состоянии.
Строгая гарантия (Strong Guarantee) – либо операция завершается успешно, либо объект остаётся в том же состоянии, что и до вызова.
Гарантия отсутствия исключений (No-Throw Guarantee) – операция никогда не бросает исключений.

🚩Что такое строгая гарантия безопасности исключений?

Строгая гарантия (Strong Guarantee) означает, что если во время выполнения операции выбрасывается исключение, объект остаётся неизменным. Это похоже на транзакции в базе данных – "всё или ничего".
#include <iostream>
#include <vector>

class BadContainer {
std::vector<int> data;
public:
void add(int value) {
data.push_back(value); // push_back() может выбросить исключение
}

void print() {
for (int x : data) std::cout << x << " ";
std::cout << std::endl;
}
};

int main() {
BadContainer c;
c.add(1);
c.add(2);
c.add(3);
c.print(); // Вывод: 1 2 3

try {
c.add(42); // Может выбросить исключение (например, нехватка памяти)
} catch (...) {
std::cout << "Ошибка!\n";
}

c.print(); // ❌ Возможно, состояние испорчено!
}


🚩Как реализовать строгую гарантию?

Чтобы добиться строгой гарантии, используем Copy & Swap:
1. Создаём временный объект.
2. Выполняем изменения на временном объекте.
3. Если всё прошло успешно – меняем указатель (swap).
#include <iostream>
#include <vector>

class SafeContainer {
std::vector<int> data;
public:
void add(int value) {
std::vector<int> temp = data; // Создаём копию текущего состояния
temp.push_back(value); // Добавляем элемент в копию
std::swap(data, temp); // Если исключения не было, меняем данные
}

void print() {
for (int x : data) std::cout << x << " ";
std::cout << std::endl;
}
};

int main() {
SafeContainer c;
c.add(1);
c.add(2);
c.add(3);
c.print(); // Вывод: 1 2 3

try {
c.add(42); // Если тут исключение, объект не изменится
} catch (...) {
std::cout << "Ошибка!\n";
}

c.print(); // ✅ Вывод: 1 2 3 (не испорчен!)
}


🚩Где используется строгая гарантия?

Операции присваивания (operator=) с Copy & Swap
Методы стандартных контейнеров (std::vector::resize, std::map::insert)
Функции стандартной библиотеки (std::sort)
Изменения объектов с ресурсами (файлы, сокеты, память)

Ставь 👍 и забирай 📚 Базу знаний
  • 👍 1
Post #2309 545
🤔 Что пришло на смену auto_ptr?

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

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2308 596
🤔 Какие есть виды полиморфизма?

В программировании, включая C++, полиморфизм (многоформенность) – это способность объекта или функции принимать разные формы. Полиморфизм является ключевой концепцией объектно-ориентированного программирования (ООП).

🚩Компиляторный (статический) полиморфизм

Этот вид полиморфизма реализуется во время компиляции. Он достигается с помощью перегрузки функций (function overloading) и перегрузки операторов (operator overloading).

🟠Перегрузка функций
В перегрузке функций одна функция имеет несколько определений с разными параметрами.
#include <iostream>

void print(int value) {
std::cout << "Целое число: " << value << std::endl;
}

void print(double value) {
std::cout << "Вещественное число: " << value << std::endl;
}

void print(const std::string& value) {
std::cout << "Строка: " << value << std::endl;
}

int main() {
print(42); // Вызов функции для int
print(3.14); // Вызов функции для double
print("Привет!"); // Вызов функции для строки
return 0;
}


🟠Перегрузка операторов
Перегрузка операторов позволяет определить, как стандартные операторы работают с пользовательскими типами данных.
#include <iostream>

class Complex {
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}

Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}

void display() const {
std::cout << real << " + " << imag << "i" << std::endl;
}
};

int main() {
Complex c1(1.0, 2.0), c2(3.0, 4.0);
Complex c3 = c1 + c2; // Используется перегрузка оператора +
c3.display(); // Вывод: 4 + 6i
return 0;
}


🚩Рантаймный (динамический) полиморфизм

Этот вид полиморфизма проявляется во время выполнения программы. Реализуется с использованием виртуальных функций и наследования.

Виртуальные функции
#include <iostream>

class Animal {
public:
virtual void sound() const { // Виртуальная функция
std::cout << "Некоторый звук" << std::endl;
}
};

class Dog : public Animal {
public:
void sound() const override { // Переопределение
std::cout << "Гав-гав" << std::endl;
}
};

class Cat : public Animal {
public:
void sound() const override { // Переопределение
std::cout << "Мяу" << std::endl;
}
};

void makeSound(const Animal& animal) {
animal.sound(); // Динамическое определение, какой sound() вызывать
}

int main() {
Dog dog;
Cat cat;

makeSound(dog); // Вывод: Гав-гав
makeSound(cat); // Вывод: Мяу
return 0;
}


Ставь 👍 и забирай 📚 Базу знаний
Post #2306 546
🤔 Как можно проверить, что нет double exception?

Используйте функцию std::uncaught_exceptions() (C++17) или std::uncaught_exception() (до C++17) для проверки активного исключения.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
  • 🔥 2
Post #2304 581
🤔 Что необходимо иметь классу, что бы использовать его в качестве ключа к ассоциативным контейнерам?

Чтобы класс мог использоваться в качестве ключа в ассоциативных контейнерах (например, в std::map или std::unordered_map), он должен удовлетворять определённым требованиям.

🚩Ассоциативные контейнеры на основе дерева

🟠Оператор сравнения
Класс должен иметь определённый оператор сравнения. По умолчанию, std::map и std::set используют оператор operator< для сравнения ключей. Это необходимо, чтобы контейнер мог упорядочивать ключи. В этом примере класс MyKey имеет перегруженный оператор operator<, что позволяет использовать его в качестве ключа в std::map.
#include <iostream>
#include <map>

class MyKey {
public:
int value;

MyKey(int v) : value(v) {}

bool operator<(const MyKey& other) const {
return value < other.value;
}
};

int main() {
std::map<MyKey, std::string> myMap;
myMap[MyKey(1)] = "one";
myMap[MyKey(2)] = "two";

for (const auto& pair : myMap) {
std::cout << pair.first.value << ": " << pair.second << std::endl;
}

return 0;
}


🚩Неупорядоченные ассоциативные контейнеры

🟠Функция хэширования
Класс должен быть хэшируемым. Это означает, что для него должна быть определена функция хэширования. В стандартной библиотеке C++ можно определить специализированный шаблон std::hash для вашего класса.

🟠Операторы сравнения на равенство
Класс должен иметь определённый оператор operator==. В этом примере для класса MyKey определён оператор operator== и специализированный шаблон std::hash, что позволяет использовать его в качестве ключа в std::unordered_map.
#include <iostream>
#include <unordered_map>
#include <functional>

class MyKey {
public:
int value;

MyKey(int v) : value(v) {}

bool operator==(const MyKey& other) const {
return value == other.value;
}
};

namespace std {
template <>
struct hash<MyKey> {
std::size_t operator()(const MyKey& k) const {
return std::hash<int>()(k.value);
}
};
}

int main() {
std::unordered_map<MyKey, std::string> myMap;
myMap[MyKey(1)] = "one";
myMap[MyKey(2)] = "two";

for (const auto& pair : myMap) {
std::cout << pair.first.value << ": " << pair.second << std::endl;
}

return 0;
}


Ставь 👍 и забирай 📚 Базу знаний
Post #2302 535
🤔 Почему по идее в конструкторе нужны исключения?

1. Конструктор отвечает за корректную инициализацию объекта, поэтому при ошибке важно сигнализировать о проблеме.
2. Исключения позволяют безопасно остановить создание объекта, если данные или ресурсы не были успешно инициализированы.
3. Это защищает от использования некорректно инициализированных объектов.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2300 557
🤔 Что можешь сказать плохого про MVC?

МVC (Model-View-Controller) – это классический паттерн разделения логики приложения, но у него есть недостатки, особенно в сложных проектах.

🚩Запутанные связи между компонентами

Взаимодействие между Model, View и Controller может стать сложным. Когда модель изменяется, нужно обновлять все представления (View), которые её используют. Если контроллер слишком тесно связан с представлением, код становится сложно поддерживать.
В сложных GUI-приложениях (например, Qt, MFC) модель может изменяться из разных мест, и контроллеру трудно управлять обновлениями.

🚩Слабая масштабируемость в больших проектах

MVC не даёт чёткого разделения ответственности, если приложение очень сложное.
Один контроллер может управлять сразу несколькими представлениями, что создаёт запутанный код. Часто приходится создавать "промежуточные" контроллеры для разруливания логики → код становится сложнее.
В сложных веб-приложениях (React, Angular) MVC превращается в "спагетти", потому что представления (View) и контроллеры (Controller) начинают выполнять логические задачи, которые должны быть в модели.
Использовать слоистую архитектуру (Layered Architecture) или Service Layer.

🚩"Толстый" контроллер (Fat Controller)

Если бизнес-логика попадает в контроллер, он становится раздутым и сложно поддерживаемым. Контроллер начинает обрабатывать данные, проверять их, а не просто передавать управление.
class UserController {
public:
void login(std::string username, std::string password) {
if (username.empty() || password.empty()) {
std::cout << "Ошибка: пустые данные\n";
return;
}

if (username == "admin" && password == "1234") {
std::cout << "Вход выполнен!\n";
} else {
std::cout << "Неверный логин/пароль\n";
}
}
};


Здесь контроллер сам проверяет данные и логику аутентификации, что нарушает принцип разделения ответственности (SRP).
Вынести бизнес-логику в Model или Service.
Использовать слоистую архитектуру (Service Layer, Repository Pattern).
class AuthService {
public:
bool authenticate(const std::string& username, const std::string& password) {
return (username == "admin" && password == "1234");
}
};

class UserController {
AuthService authService;
public:
void login(const std::string& username, const std::string& password) {
if (authService.authenticate(username, password)) {
std::cout << "Вход выполнен!\n";
} else {
std::cout << "Неверный логин/пароль\n";
}
}
};


🚩Трудности тестирования (особенно View и Controller)

Model тестируется легко, потому что она "чистая" (без UI-кода).
Controller и View сложно тестировать, потому что они зависят от пользовательского ввода и интерфейса.
Unit-тестирование UI-кода практически невозможно, требуется мокирование.

🚩Не всегда подходит для многопоточных приложений

В многопоточных приложениях несколько представлений могут обращаться к одной модели, вызывая гонки данных.
Если контроллер один на несколько потоков, он становится "узким местом" (bottleneck).

Ставь 👍 и забирай 📚 Базу знаний
Post #2298 477
🤔 Какие можно выделить моменты, которые поменяли в 11-ом стандарте в лучшую сторону?

1. Move semantics: добавлены rvalue-ссылки и std::move для оптимизации копирования.
2. Lambda-выражения: упростили работу с функциями.
3. auto: автоматическое определение типов для удобства разработки.
4. Unordered контейнеры: такие как unordered_map и unordered_set, ускорили работу с большими наборами данных.
5. Multithreading: появился стандартный std::thread и библиотека <future> для работы с потоками.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2296 546
🤔 Когда возникает самый худший случай поиска в unordered_set?

В unordered_set, который основан на хеш-таблице, самый худший случай поиска возникает, когда структура данных деградирует до такой степени, что время поиска становится линейным, то есть \(O(n)\), где \(n\) — количество элементов в контейнере. Это происходит по нескольким причинам:

🟠Плохая хеш-функция
Играет ключевую роль в распределении элементов по "ведрам" (buckets) в unordered_set. Если хеш-функция плохо разрабатывается и генерирует одинаковые или близкие хеш-коды для большого количества ключей, элементы начинают накапливаться в очень небольшом числе ведер. Это приводит к тому, что большинство операций поиска, вставки и удаления начинают зависеть от числа элементов в самом длинном связном списке, а не от общего количества ведер, что сильно ухудшает производительность.

🟠Высокая степень загрузки
Загрузка хеш-таблицы (load factor) — это отношение количества элементов в хеш-таблице к количеству ведер. Когда load factor становится слишком высоким, вероятность коллизий увеличивается, что также приводит к увеличению длины цепочек в каждом ведре. Хотя unordered_set автоматически увеличивает количество ведер при увеличении количества элементов, в случаях с экстремально высоким load factor поиск может деградировать до линейного.

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

Ставь 👍 и забирай 📚 Базу знаний
Post #2295 525
🤔 В чем разница set и unordered_set?

Set — это отсортированное множество, которое хранит элементы в порядке возрастания и использует бинарное дерево для внутренней реализации. Unordered_set хранит элементы в произвольном порядке и использует хеш-таблицу для доступа к элементам. В set операции поиска, вставки и удаления имеют сложность O(log n), а в unordered_set — O(1) в среднем случае, но O(n) в худшем случае. Set предпочтителен, когда требуется поддерживать порядок элементов.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2293 601
🤔 Коллизия - когда для разных ключей получается одно и тоже хэш значение?

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

🚩Причины возникновения

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

🚩Способы решения

🟠Цепочки (Chaining)
В этом методе каждая ячейка хеш-таблицы содержит указатель на список (или другую структуру данных, например, связный список), которые хранят все элементы, имеющие одинаковый хеш. Это простой и эффективный метод, но может потребовать больше памяти и времени при обращении к элементам.

🟠Открытая адресация (Open Addressing)
Включает в себя методы, такие как линейное пробирование, квадратичное пробирование и двойное хеширование. При возникновении коллизии метод находит следующую свободную ячейку в таблице для размещения нового элемента.

🟠Двойное хеширование (Double Hashing)
Использует вторую хеш-функцию для определения интервала между пробами, уменьшая вероятность коллизий, которые происходят при первичном хешировании.

Ставь 👍 и забирай 📚 Базу знаний
Post #2291 518
🤔 Что такое noexcept?

`noexcept` — это спецификатор в C++, который указывает, что функция не будет выбрасывать исключений. Это помогает компилятору оптимизировать код и предотвратить ненужные проверки на выброс исключений. Функции, помеченные как `noexcept`, могут быть безопасно вызваны в контексте, где выбрасывание исключений может привести к сбою программы. Использование `noexcept` также улучшает производительность в ситуациях, связанных с исключительным управлением.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
  • 👍 1
Post #2289 456
🤔 Зачем нам нужна move семантика?

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

🚩Зачем она нужна?

🟠Эффективность работы с ресурсами
Копирование больших объектов может быть очень затратным. Move семантика позволяет перенести ресурсы от одного объекта к другому без дорогостоящего копирования.

🟠Улучшение производительности
Перемещение (move) быстрее копирования, поскольку оно всего лишь переназначает указатели на ресурсы, вместо создания их копий. Это особенно важно в приложениях с высокой производительностью, таких как игры, обработка видео, базы данных.

🚩Как это используется?

Move семантика реализуется с помощью rvalue ссылок (ссылок на временные объекты) и специальных методов — move конструктора и move оператора присваивания.
#include <iostream>
#include <vector>

class MyClass {
public:
int* data;
size_t size;

// Конструктор
MyClass(size_t s) : size(s), data(new int[s]) {
std::cout << "Constructing MyClass\n";
}

// Деструктор
~MyClass() {
delete[] data;
std::cout << "Destructing MyClass\n";
}

// Move конструктор
MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; // Обнуляем указатель у "старого" объекта
other.size = 0;
std::cout << "Move constructing MyClass\n";
}

// Move оператор присваивания
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data; // Освобождаем старый ресурс
data = other.data;
size = other.size;
other.data = nullptr; // Обнуляем указатель у "старого" объекта
other.size = 0;
std::cout << "Move assigning MyClass\n";
}
return *this;
}
};

int main() {
MyClass a(10); // Создаем объект a
MyClass b = std::move(a); // Перемещаем ресурсы от a к b

return 0;
}


Ставь 👍 и забирай 📚 Базу знаний
Post #2287 478
🤔 Зачем нужен виртуальный деструктор?

Виртуальный деструктор необходим, когда класс предполагает полиморфное использование, то есть когда объекты базового класса могут быть удалены через указатель на производный класс. Без виртуального деструктора вызов `delete` на указатель базового типа приведет к вызову только деструктора базового класса, что может вызвать утечку памяти для производных классов. Виртуальный деструктор гарантирует, что будет вызван правильный деструктор для производного класса, обеспечивая корректное освобождение всех ресурсов.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2285 461
🤔 Что можешь сказать про исключение в конструкторе?

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

🚩Что происходит при исключении в конструкторе?

Когда исключение возникает в конструкторе:
Объект не создаётся – вызов конструктора считается неудачным.
Деструктор НЕ вызывается – потому что объект не был полностью создан.
Уже созданные члены класса должны быть очищены вручную (если используются RAII-объекты, они сами очистят ресурсы).
Исключение должно быть обработано – либо перехвачено (try-catch), либо передано дальше.

🚩Пример исключения в конструкторе

#include <iostream>
#include <stdexcept>

class MyClass {
public:
MyClass() {
std::cout << "Конструктор\n";
throw std::runtime_error("Ошибка в конструкторе!");
}

~MyClass() {
std::cout << "Деструктор\n";
}
};

int main() {
try {
MyClass obj; // Попытка создать объект
} catch (const std::exception& e) {
std::cout << "Поймано исключение: " << e.what() << '\n';
}
}


Вывод программы
Конструктор
Поймано исключение: Ошибка в конструкторе!


🚩Как правильно работать с исключениями в конструкторе?

🟠Использовать RAII
Лучший способ избежать утечек – использовать RAII (объекты, которые сами освобождают ресурсы в деструкторе).
#include <iostream>
#include <memory>
#include <stdexcept>

class MyClass {
private:
std::unique_ptr<int> data; // Умный указатель RAII
public:
MyClass() : data(std::make_unique<int>(42)) {
throw std::runtime_error("Ошибка!");
}
};

int main() {
try {
MyClass obj;
} catch (const std::exception& e) {
std::cout << "Ошибка: " << e.what() << '\n';
}
}


🟠Использовать `try-catch` внутри конструктора
Если важно, чтобы объект всегда создавался, можно перехватывать исключение прямо в конструкторе.
class MyClass {
public:
MyClass() {
try {
throw std::runtime_error("Ошибка!");
} catch (...) {
std::cout << "Ошибка обработана, объект создан\n";
}
}
};


🟠Использовать `noexcept(false)` или `noexcept(true)`
По умолчанию конструкторы могут выбрасывать исключения, но если мы точно знаем, что этого не произойдёт, лучше явно указать noexcept:
class SafeClass {
public:
SafeClass() noexcept {
std::cout << "Конструктор без исключений\n";
}
};


Ставь 👍 и забирай 📚 Базу знаний
Post #2284 490
🤔 Сколько будет таблиц виртуальных функций у двух классов, участвующих в наследовании?

Если у базового класса есть хотя бы одна виртуальная функция, то у него создается одна таблица виртуальных функций (vtable). У производного класса также создается своя vtable, если он переопределяет виртуальные методы или добавляет новые.
- Если производный класс не добавляет новых виртуальных функций, он использует vtable родительского класса.
- Если переопределяет методы, создается отдельная vtable для производного класса.
Таким образом, в общем случае будет две таблицы vtable – по одной для каждого класса.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
  • 👍 1
  • 🔥 1
Post #2282 489
🤔 Что такое deque?

Deque (double-ended queue, двусторонняя очередь) — это последовательный контейнер в C++, который позволяет добавлять и удалять элементы как с начала, так и с конца очереди с постоянной временной сложностью O(1). Контейнер deque является частью стандартной библиотеки шаблонов (STL) и объявляется в заголовочном файле <deque>.

🚩Основные характеристики:

🟠Быстрый доступ к элементам:
deque обеспечивает доступ к элементам по индексу с амортизированной временной сложностью O(1).
🟠Двусторонняя очередь:
Позволяет эффективно добавлять и удалять элементы как с начала, так и с конца очереди.
🟠Динамический размер:
deque автоматически изменяет свой размер по мере добавления или удаления элементов, подобно vector.
🟠Не требует смежного хранения:
В отличие от vector, элементы deque могут быть размещены в различных сегментах памяти, что делает его более гибким для частых операций вставки и удаления.

🚩Основные операции:

🟠Добавление элементов:
push_back(), push_front()
🟠Удаление элементов:
pop_back(), pop_front()
🟠Доступ к элементам:
operator[], at(), front(), back()
🟠Размер и емкость:
size(), empty(), resize()
🟠Итераторы:
Поддержка итераторов для прохода по элементам
#include <iostream>
#include <deque>

int main() {
std::deque<int> dq;

// Добавление элементов в конец
dq.push_back(1);
dq.push_back(2);
dq.push_back(3);

// Добавление элементов в начало
dq.push_front(0);

std::cout << "Deque elements: ";
for (int elem : dq) {
std::cout << elem << " ";
}
std::cout << std::endl;

// Удаление элемента с конца
dq.pop_back();

// Удаление элемента с начала
dq.pop_front();

std::cout << "Deque elements after pop operations: ";
for (int elem : dq) {
std::cout << elem << " ";
}
std::cout << std::endl;

return 0;
}


Ставь 👍 и забирай 📚 Базу знаний
Older posts →
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →