Функции floor() и ceil() на C++ занимаются не только округлением чисел. Изучим их эффективное использование в реальных сценариях  —  от финансовых расчетов до разработки игр.

Базовое применение floor() и ceil()

Сначала посмотрим на эти функции в деле:

#include <cmath>
#include <iostream>

void basic_examples() {
double numbers[] = {3.7, -3.7, 0.0, -0.0, 3.0, -3.0};

for (double num : numbers) {
std::cout << "Number: " << num << '\n'
<< "Floor: " << std::floor(num) << '\n'
<< "Ceil: " << std::ceil(num) << '\n'
<< "-------------------\n";
}
}

// Вывод:
// Число: 3.7
// Пол: 3
// Потолок: 4
// -------------------
// Число: -3.7
// Пол: -4
// Потолок: -3
// -------------------
// ... и так далее

Реальный пример: финансовые расчеты

Вот как используются floor() и ceil() для финансового округления:

class MoneyCalculator {
public:
// Округляется до цента
static double round_currency(double amount) {
return std::floor(amount * 100 + 0.5) / 100;
}

// Округляются вверх до цента, например комиссии
static double ceil_currency(double amount) {
return std::ceil(amount * 100) / 100;
}

// Округляются вниз до цента, например дисконт
static double floor_currency(double amount) {
return std::floor(amount * 100) / 100;
}
};

void financial_example() {
double price = 19.999;
double tax_rate = 0.08; // 8%-ный налог

double tax = MoneyCalculator::ceil_currency(price * tax_rate);
double final_price = MoneyCalculator::round_currency(price + tax);

std::cout << "Original price: $" << price << '\n'
<< "Tax: $" << tax << '\n'
<< "Final price: $" << final_price << '\n';
}

Разработка игр: сеточное позиционирование

Вот как floor() и ceil() используются в сеточной игровой механике:

struct Vector2D {
double x, y;

Vector2D(double x_, double y_) : x(x_), y(y_) {}
};

class GridSystem {
public:
static constexpr double CELL_SIZE = 64.0; // пиксели

static Vector2D world_to_grid(const Vector2D& world_pos) {
return Vector2D(
std::floor(world_pos.x / CELL_SIZE),
std::floor(world_pos.y / CELL_SIZE)
);
}

static Vector2D grid_to_world(const Vector2D& grid_pos) {
return Vector2D(
grid_pos.x * CELL_SIZE,
grid_pos.y * CELL_SIZE
);
}

static Vector2D snap_to_grid(const Vector2D& world_pos) {
Vector2D grid_pos = world_to_grid(world_pos);
return grid_to_world(grid_pos);
}
};

void game_example() {
Vector2D player_pos(123.45, 67.89);
Vector2D grid_pos = GridSystem::world_to_grid(player_pos);
Vector2D snapped_pos = GridSystem::snap_to_grid(player_pos);

std::cout << "Player position: (" << player_pos.x << ", " << player_pos.y << ")\n"
<< "Grid position: (" << grid_pos.x << ", " << grid_pos.y << ")\n"
<< "Snapped position: (" << snapped_pos.x << ", " << snapped_pos.y << ")\n";
}

Пользовательские функции округления

Вот набор полезных утилит округления:

class RoundingUtils {
public:
// Округляются до ближайшего кратного
static double round_to_multiple(double value, double multiple) {
return std::floor(value / multiple + 0.5) * multiple;
}

// Округляются вверх до ближайшего кратного
static double ceil_to_multiple(double value, double multiple) {
return std::ceil(value / multiple) * multiple;
}

// Округляются вниз до ближайшего кратного
static double floor_to_multiple(double value, double multiple) {
return std::floor(value / multiple) * multiple;
}

// Округляются до указанных знаков после запятой
static double round_to_decimal(double value, int decimal_places) {
double multiplier = std::pow(10.0, decimal_places);
return std::floor(value * multiplier + 0.5) / multiplier;
}
};

void demonstrate_rounding() {
double value = 47.123;

std::cout << "Original value: " << value << '\n'
<< "Rounded to nearest 5: " << RoundingUtils::round_to_multiple(value, 5.0) << '\n'
<< "Ceiling to nearest 5: " << RoundingUtils::ceil_to_multiple(value, 5.0) << '\n'
<< "Floor to nearest 5: " << RoundingUtils::floor_to_multiple(value, 5.0) << '\n'
<< "Rounded to 2 decimals: " << RoundingUtils::round_to_decimal(value, 2) << '\n';
}

Пример распределения ресурсов

Вот как при помощи ceil() рассчитывается выделение памяти:

class ResourceAllocator {
public:
static constexpr size_t BLOCK_SIZE = 4096; // Блоки 4 Кб

static size_t calculate_blocks_needed(size_t bytes) {
return static_cast<size_t>(std::ceil(static_cast<double>(bytes) / BLOCK_SIZE));
}

static size_t calculate_allocation_size(size_t bytes) {
return calculate_blocks_needed(bytes) * BLOCK_SIZE;
}

static void print_allocation_info(size_t requested_bytes) {
size_t blocks = calculate_blocks_needed(requested_bytes);
size_t actual_bytes = calculate_allocation_size(requested_bytes);

std::cout << "Requested: " << requested_bytes << " bytes\n"
<< "Blocks needed: " << blocks << '\n'
<< "Actual allocation: " << actual_bytes << " bytes\n"
<< "Wasted space: " << actual_bytes - requested_bytes << " bytes\n";
}
};

void allocation_example() {
ResourceAllocator::print_allocation_info(6000); // Больше, чем один блок
ResourceAllocator::print_allocation_info(2048); // Меньше, чем один блок
}

Работа с временными интервалами

А так floor() и ceil() справляются с вычислениями времени:

class TimeCalculator {
public:
static constexpr int SECONDS_PER_MINUTE = 60;
static constexpr int SECONDS_PER_HOUR = 3600;

static int ceil_minutes(double seconds) {
return static_cast<int>(std::ceil(seconds / SECONDS_PER_MINUTE));
}

static int floor_minutes(double seconds) {
return static_cast<int>(std::floor(seconds / SECONDS_PER_MINUTE));
}

static std::string format_time_range(double seconds) {
int min_minutes = floor_minutes(seconds);
int max_minutes = ceil_minutes(seconds);

if (min_minutes == max_minutes) {
return std::to_string(min_minutes) + " minutes";
}

return std::to_string(min_minutes) + "-" +
std::to_string(max_minutes) + " minutes";
}

static int calculate_billing_units(double hours, double unit_size = 0.25) {
return static_cast<int>(std::ceil(hours / unit_size));
}
};

void time_example() {
double task_duration = 127.5; // секунды

std::cout << "Task will take: " << TimeCalculator::format_time_range(task_duration) << '\n';

double meeting_hours = 1.6; // один час 36 минут
int billing_units = TimeCalculator::calculate_billing_units(meeting_hours);
std::cout << "Billable units (15-minute intervals): " << billing_units << '\n';
}

Типичные проблемы и их решения

  1. Проблемы точности вычислений с плавающей точкой:
double value = 2.0;
// Не делайте этого:
if (std::floor(value) == value) { // Не выполнится из-за точности
std::cout << "Integer value\n";
}

// Делайте так:
const double epsilon = 1e-10;
if (std::abs(std::floor(value) - value) < epsilon) {
std::cout << "Integer value\n";
}

2. Целочисленное переполнение при приведении типов:

class SafeRounding {
public:
static int safe_floor(double value) {
if (value > std::numeric_limits<int>::max() ||
value < std::numeric_limits<int>::min()) {
throw std::overflow_error("Value out of integer range");
}
return static_cast<int>(std::floor(value));
}
};

Не забывайте, что floor() и ceil() предназначены для типов с плавающей точкой float, double, long double. Для целочисленных типов эти операции излишни, поскольку целые числа уже «округлены» по определению.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: Floor and Ceil Functions in C++: Complete Guide

Предыдущая статьяПочему typeof null === ‘object’
Следующая статьяПроектирование системы управления состояниями для повышения производительности: иерархическая реактивность