Semana 5 — Listas enlazadas y deques
Objetivos
- Implementar listas simple y doblemente enlazadas, con ownership claro.
- Escribir un iterador que funcione con
forpor rango. - Implementar un mismo tipo abstracto con dos representaciones y compararlas midiendo.
Teoría — Listas enlazadas e iteradores · Deques
Lectura: Apuntes, semana 5.
Apoyo: Open Data Structures, secs. 2.4 (ArrayDeque), 3.1 (SLList) y 3.2 (DLList). CLRS, capítulo Elementary Data Structures.
Temas:
- Nodos en la memoria: cada uno por su lado, unidos por punteros. Dibujar antes de programar.
- Lista simple: insertar y sacar al inicio en O(1). Llegar al elemento i cuesta Θ(i).
- Lista doble con nodo centinela: menos casos especiales. Quién es el owner de cada nodo y cómo liberarlos todos.
- Iteradores:
begin,end,operator*,operator++yoperator!=. Qué hace realmente unforpor rango. - Cuándo se invalida un iterador en una lista y cuándo en un arreglo dinámico.
- El tipo abstracto deque: insertar y sacar por los dos extremos. Pilas y colas son casos particulares.
- Dos representaciones con el mismo O(1): buffer circular y lista doble. Lo que O no ve: reservas de memoria y cache.
- Error común: borrar un nodo y luego leer su
nextpara avanzar.
Laboratorio — Un deque con dos representaciones
Sesión 1:
- Escribir
ArrayDeque<T>sobre un buffer circular que crece. - Correr las pruebas dadas, incluida una con operaciones al azar y semilla fija, que compara contra
std::deque.
Sesión 2:
3. Escribir LinkedDeque<T> con lista doble y centinela, con la misma interfaz y las mismas pruebas, y con su iterador.
4. Medir las dos: operaciones en los extremos y recorrer todo sumando los elementos, con n creciente. Reenlazar los nodos de la lista en orden al azar, con shuffle_nodes, y volver a medir.
5. Escribir una conclusión: las dos operaciones son O(1) y O(n), ¿por qué los tiempos no son iguales?
Preguntas de repaso
- Dibujen una lista doble con centinela y dos elementos. ¿Qué punteros cambian al sacar el primero?
- ¿Qué ventaja da el centinela? ¿Qué casos especiales desaparecen?
- ¿Qué operación es O(1) en una lista y Θ(n) en un arreglo? ¿Cuál es al revés?
- ¿Qué pasa con un iterador a un elemento de una lista cuando se inserta otro? ¿Y en un arreglo dinámico?
- ¿Por qué recorrer una lista puede ser mucho más lento que recorrer un arreglo del mismo tamaño, si los dos son Θ(n)?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (15)Errores (4)Pruebas (4)Laboratorio (7)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Cuántas veces se llama a operator new al insertar un millón de enteros al final.
// Son conteos, no tiempos. Los de la biblioteca estándar dependen de su implementación.
#include <cstddef>
#include <deque>
#include <iostream>
#include <list>
#include <vector>
#include "alloc_counter.h"
#include "dlist.h"
namespace {
const int N = 1000000;
template <typename Sequence>
std::size_t count_allocations() {
std::size_t before = allocation_count();
Sequence sequence;
for (int i = 0; i < N; i++) {
sequence.push_back(i);
}
return allocation_count() - before;
}
} // namespace
int main() {
std::cout << "reservas para " << N << " push_back\n";
std::cout << "std::vector: " << count_allocations<std::vector<int>>() << "\n";
std::cout << "std::deque: " << count_allocations<std::deque<int>>() << "\n";
std::cout << "std::list: " << count_allocations<std::list<int>>() << "\n";
std::cout << "DList: " << count_allocations<DList<int>>() << "\n";
return 0;
}// Cuenta las reservas de memoria de un programa, reemplazando el operator new global.
// Se incluye en un solo .cpp de cada programa.
#ifndef ALLOC_COUNTER_H
#define ALLOC_COUNTER_H
#include <cstddef>
#include <cstdlib>
#include <new>
// Número de llamadas a operator new desde que empezó el programa.
inline std::size_t& allocation_count() {
static std::size_t count = 0;
return count;
}
void* operator new(std::size_t size) {
allocation_count()++;
void* block = std::malloc(size == 0 ? 1 : size);
if (block == nullptr) {
throw std::bad_alloc();
}
return block;
}
void operator delete(void* block) noexcept {
std::free(block);
}
void operator delete(void* block, std::size_t) noexcept {
std::free(block);
}
#endif// Medidor de tiempos del curso. Compilar con -O2 y sin sanitizers.
#ifndef BENCH_H
#define BENCH_H
#include <algorithm>
#include <chrono>
#include <vector>
// Corre fn `reps` veces y devuelve la mediana en segundos.
template <typename Fn>
double median_seconds(int reps, Fn fn) {
std::vector<double> times;
times.reserve(reps);
for (int i = 0; i < reps; i++) {
auto start = std::chrono::steady_clock::now();
fn();
auto stop = std::chrono::steady_clock::now();
times.push_back(std::chrono::duration<double>(stop - start).count());
}
std::sort(times.begin(), times.end());
return times[times.size() / 2];
}
// Evita que el compilador borre un cálculo cuyo resultado no se usa.
template <typename T>
void keep(const T& value) {
volatile T sink = value;
(void)sink;
}
#endif// La lista doble: insertar y borrar en cualquier punto, hacia adelante y hacia atrás.
#include <iostream>
#include "dlist.h"
namespace {
void print(const DList<int>& list) {
std::cout << "adelante:";
for (int value : list) {
std::cout << " " << value;
}
std::cout << "\natrás: ";
auto it = list.end();
while (it != list.begin()) {
--it;
std::cout << " " << *it;
}
std::cout << "\n";
}
} // namespace
int main() {
DList<int> list;
list.push_back(20);
list.push_back(40);
list.push_front(10);
print(list);
// Inserta antes del tercer elemento.
auto it = list.begin();
++it;
++it;
auto thirty = list.insert(it, 30);
print(list);
// Borra el 30 y sigue desde el siguiente.
auto after = list.erase(thirty);
std::cout << "después de borrar: " << *after << "\n";
print(list);
list.pop_front();
list.pop_back();
list.pop_back();
std::cout << "tamaño " << list.size() << ", vacía: " << (list.empty() ? "sí" : "no") << "\n";
list.check_invariants();
return 0;
}// Lista doblemente enlazada con nodo centinela, con iterador.
#ifndef DLIST_H
#define DLIST_H
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <random>
#include <vector>
// Invariantes:
// - sentinel_ nunca es nulo. Su value no es un elemento de la lista.
// - Los nodos forman un ciclo: para todo nodo x, x->next->prev == x y x->prev->next == x.
// - Siguiendo next desde sentinel_ se visitan size_ nodos antes de volver a sentinel_.
//
// T debe tener constructor por defecto, porque el centinela también guarda un T.
template <typename T>
class DList {
struct Node {
T value{};
Node* prev = nullptr;
Node* next = nullptr;
};
public:
// Con V = T se puede escribir el elemento; con V = const T, solo leerlo.
template <typename V>
class Iter {
public:
V& operator*() const { return node_->value; }
Iter& operator++() {
node_ = node_->next;
return *this;
}
Iter& operator--() {
node_ = node_->prev;
return *this;
}
bool operator==(const Iter& other) const { return node_ == other.node_; }
bool operator!=(const Iter& other) const { return node_ != other.node_; }
private:
friend class DList;
explicit Iter(Node* node) : node_(node) {}
Node* node_;
};
using iterator = Iter<T>;
using const_iterator = Iter<const T>;
DList() : sentinel_(new Node) {
sentinel_->prev = sentinel_;
sentinel_->next = sentinel_;
}
// La lista es owner de todos sus nodos, también del centinela.
~DList() {
Node* node = sentinel_->next;
while (node != sentinel_) {
Node* next = node->next; // se lee antes de borrar
delete node;
node = next;
}
delete sentinel_;
}
// Copiar y mover quedan prohibidos aquí. La regla de cinco va en el laboratorio.
DList(const DList&) = delete;
DList& operator=(const DList&) = delete;
std::size_t size() const { return size_; }
bool empty() const { return size_ == 0; }
iterator begin() { return iterator(sentinel_->next); }
iterator end() { return iterator(sentinel_); }
const_iterator begin() const { return const_iterator(sentinel_->next); }
const_iterator end() const { return const_iterator(sentinel_); }
// Inserta antes de pos y devuelve un iterador al elemento nuevo.
iterator insert(iterator pos, const T& value) {
Node* after = pos.node_;
Node* before = after->prev;
Node* node = new Node{value, before, after};
before->next = node;
after->prev = node;
size_++;
return iterator(node);
}
// Borra el elemento de pos y devuelve un iterador al siguiente.
iterator erase(iterator pos) {
assert(pos != end());
Node* node = pos.node_;
Node* after = node->next;
node->prev->next = after;
after->prev = node->prev;
delete node;
size_--;
return iterator(after);
}
void push_front(const T& value) { insert(begin(), value); }
void push_back(const T& value) { insert(end(), value); }
void pop_front() { erase(begin()); }
void pop_back() { erase(iterator(sentinel_->prev)); }
T& front() {
assert(!empty());
return sentinel_->next->value;
}
T& back() {
assert(!empty());
return sentinel_->prev->value;
}
// Vuelve a enlazar los nodos en orden aleatorio, sin moverlos en la memoria.
// Los elementos quedan en otro orden. Solo sirve para medir.
void shuffle_nodes(unsigned seed) {
std::vector<Node*> nodes;
nodes.reserve(size_);
for (Node* node = sentinel_->next; node != sentinel_; node = node->next) {
nodes.push_back(node);
}
std::mt19937 rng(seed);
std::shuffle(nodes.begin(), nodes.end(), rng);
Node* last = sentinel_;
for (Node* node : nodes) {
last->next = node;
node->prev = last;
last = node;
}
last->next = sentinel_;
sentinel_->prev = last;
}
void check_invariants() const {
assert(sentinel_ != nullptr);
std::size_t count = 0;
const Node* node = sentinel_;
do {
assert(node->next->prev == node);
assert(node->prev->next == node);
node = node->next;
count++;
assert(count <= size_ + 1);
} while (node != sentinel_);
assert(count == size_ + 1);
}
private:
Node* sentinel_;
std::size_t size_ = 0;
};
#endif// Operaciones en un extremo: un arreglo con capacidad que crece frente a una lista doble.
// Compilar con -O2 y sin sanitizers. Los tiempos dependen de la máquina.
#include <cstddef>
#include <cstdio>
#include <vector>
#include "bench.h"
#include "dlist.h"
namespace {
const int REPS = 7;
template <typename Sequence>
double push_then_pop(std::size_t n) {
double seconds = median_seconds(REPS, [&] {
Sequence sequence;
for (std::size_t i = 0; i < n; i++) {
sequence.push_back(static_cast<int>(i));
}
while (!sequence.empty()) {
keep(sequence.back());
sequence.pop_back();
}
});
return seconds * 1e9 / static_cast<double>(2 * n);
}
} // namespace
int main() {
std::printf("%8s %10s %8s %7s\n", "n", "std::vector", "DList", "razón");
for (std::size_t n = 10000; n <= 1000000; n *= 10) {
double array_ns = push_then_pop<std::vector<int>>(n);
double list_ns = push_then_pop<DList<int>>(n);
std::printf("%8zu %10.2f %8.2f %6.1f\n", n, array_ns, list_ns, list_ns / array_ns);
}
return 0;
}// Borrar mientras se recorre, de la forma correcta: erase devuelve el siguiente.
#include <iostream>
#include "dlist.h"
int main() {
DList<int> list;
for (int i = 1; i <= 8; i++) {
list.push_back(i);
}
auto it = list.begin();
while (it != list.end()) {
if (*it % 2 == 0) {
it = list.erase(it);
} else {
++it;
}
}
for (int value : list) {
std::cout << value << " ";
}
std::cout << "\n";
list.check_invariants();
return 0;
}// Qué sobrevive a una inserción: en una lista y en un arreglo dinámico.
// Se comparan direcciones guardadas como números: nunca se usa un puntero inválido.
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <vector>
#include "dlist.h"
namespace {
std::intptr_t address(const int& value) {
return reinterpret_cast<std::intptr_t>(&value);
}
const char* yes_no(bool value) {
return value ? "sí" : "no";
}
} // namespace
int main() {
DList<int> list;
list.push_back(1);
int* in_list = &list.front();
auto it = list.begin();
for (int i = 0; i < 1000; i++) {
list.push_back(i);
list.push_front(i);
}
std::cout << "lista: el elemento sigue en la misma dirección: " << yes_no(in_list == &*it) << "\n";
std::cout << "lista: *it = " << *it << ", *in_list = " << *in_list << "\n";
std::vector<int> grows;
grows.push_back(1);
std::intptr_t before = address(grows[0]);
std::size_t moved_times = 0;
for (int i = 0; i < 1000; i++) {
grows.push_back(i);
if (address(grows[0]) != before) {
moved_times++;
before = address(grows[0]);
}
}
std::cout << "vector sin reserve: el bloque cambió de lugar " << moved_times << " veces\n";
std::vector<int> reserved;
reserved.reserve(1001);
reserved.push_back(1);
before = address(reserved[0]);
for (int i = 0; i < 1000; i++) {
reserved.push_back(i);
}
std::cout << "vector con reserve: siguió en la misma dirección: "
<< yes_no(address(reserved[0]) == before) << "\n";
return 0;
}// Dónde quedan los elementos en la memoria: distancia de cada uno al primero.
// Las distancias dependen de la biblioteca y de la máquina.
#include <cstdint>
#include <iostream>
#include <vector>
#include "dlist.h"
namespace {
std::intptr_t address(const int& value) {
return reinterpret_cast<std::intptr_t>(&value);
}
void print_offsets(const char* name, const DList<int>& list) {
std::cout << name << ":";
std::intptr_t first = address(*list.begin());
for (const int& value : list) {
std::cout << " " << address(value) - first;
}
std::cout << "\n";
}
} // namespace
int main() {
const int n = 6;
std::vector<int> values(n);
std::cout << "arreglo:";
for (const int& value : values) {
std::cout << " " << address(value) - address(values[0]);
}
std::cout << "\n";
DList<int> list;
for (int i = 0; i < n; i++) {
list.push_back(i);
}
print_offsets("lista", list);
// Otras reservas entre nodo y nodo, como en un programa que hace varias cosas a la vez.
DList<int> mixed;
std::vector<std::vector<int>> others;
others.reserve(n);
for (int i = 0; i < n; i++) {
mixed.push_back(i);
others.push_back(std::vector<int>(10 + 10 * i));
}
print_offsets("con otras reservas", mixed);
list.shuffle_nodes(1);
print_offsets("lista barajada", list);
return 0;
}// Una misma prueba para varias representaciones: el template pide operaciones, no una clase base.
#include <cassert>
#include <deque>
#include <iostream>
#include <list>
#include "dlist.h"
// Compila con cualquier D que tenga estas operaciones.
template <typename D>
void test_both_ends() {
D deque;
assert(deque.empty());
deque.push_back(2);
deque.push_back(3);
deque.push_front(1);
assert(deque.size() == 3);
assert(deque.front() == 1);
assert(deque.back() == 3);
int expected = 1;
for (int value : deque) {
assert(value == expected);
expected++;
}
deque.pop_front();
deque.pop_back();
assert(deque.size() == 1);
assert(deque.front() == 2);
assert(deque.back() == 2);
}
int main() {
test_both_ends<DList<int>>();
std::cout << "DList: ok\n";
test_both_ends<std::list<int>>();
std::cout << "std::list: ok\n";
test_both_ends<std::deque<int>>();
std::cout << "std::deque: ok\n";
return 0;
}// Una lista simple: insertar y sacar por el inicio, y llegar al elemento i.
#include <cstddef>
#include <iostream>
#include <string>
#include "slist.h"
namespace {
void print(SinglyList<std::string>& list) {
std::cout << "[";
for (std::size_t i = 0; i < list.size(); i++) {
std::cout << (i == 0 ? "" : " ") << list.at(i);
}
std::cout << "] tamaño " << list.size() << "\n";
}
} // namespace
int main() {
SinglyList<std::string> list;
list.push_front("lima");
list.push_front("cusco");
list.push_front("puno");
print(list);
list.pop_front();
print(list);
list.push_back("tacna");
print(list);
list.check_invariants();
std::cout << "at(2) = " << list.at(2) << "\n";
return 0;
}// Lista simplemente enlazada, con puntero al primero y al último.
#ifndef SLIST_H
#define SLIST_H
#include <cassert>
#include <cstddef>
// Invariantes:
// - head_ es nulo si y solo si tail_ es nulo si y solo si size_ == 0.
// - Siguiendo next desde head_ se visitan size_ nodos, y el último es tail_.
// - tail_->next es nulo.
template <typename T>
class SinglyList {
public:
SinglyList() = default;
// La lista es owner de sus nodos: este ciclo los libera todos.
~SinglyList() {
while (head_ != nullptr) {
pop_front();
}
}
// Copiar y mover quedan prohibidos para no distraernos. La regla de cinco va en el laboratorio.
SinglyList(const SinglyList&) = delete;
SinglyList& operator=(const SinglyList&) = delete;
std::size_t size() const { return size_; }
bool empty() const { return size_ == 0; }
T& front() {
assert(!empty());
return head_->value;
}
void push_front(const T& value) {
head_ = new Node{value, head_};
if (tail_ == nullptr) {
tail_ = head_;
}
size_++;
}
void pop_front() {
assert(!empty());
Node* old_head = head_;
head_ = old_head->next;
delete old_head;
if (head_ == nullptr) {
tail_ = nullptr;
}
size_--;
}
void push_back(const T& value) {
Node* node = new Node{value, nullptr};
if (tail_ == nullptr) {
head_ = node;
} else {
tail_->next = node;
}
tail_ = node;
size_++;
}
// Llegar al elemento i cuesta i saltos.
T& at(std::size_t i) {
assert(i < size_);
Node* node = head_;
for (std::size_t step = 0; step < i; step++) {
node = node->next;
}
return node->value;
}
void check_invariants() const {
assert((head_ == nullptr) == (size_ == 0));
assert((tail_ == nullptr) == (size_ == 0));
std::size_t count = 0;
const Node* last = nullptr;
for (const Node* node = head_; node != nullptr; node = node->next) {
last = node;
count++;
assert(count <= size_);
}
assert(count == size_);
assert(last == tail_);
}
private:
struct Node {
T value;
Node* next;
};
Node* head_ = nullptr;
Node* tail_ = nullptr;
std::size_t size_ = 0;
};
#endif// Qué llamadas hace realmente un for por rango: un iterador que avisa cada vez que lo usan.
#include <iostream>
namespace {
class TraceIterator {
public:
explicit TraceIterator(int current) : current_(current) {}
int operator*() const {
std::cout << " operator*\n";
return current_;
}
TraceIterator& operator++() {
std::cout << " operator++\n";
current_++;
return *this;
}
bool operator!=(const TraceIterator& other) const {
std::cout << " operator!=\n";
return current_ != other.current_;
}
private:
int current_;
};
class TraceRange {
public:
explicit TraceRange(int count) : count_(count) {}
TraceIterator begin() const {
std::cout << " begin\n";
return TraceIterator(0);
}
TraceIterator end() const {
std::cout << " end\n";
return TraceIterator(count_);
}
private:
int count_;
};
} // namespace
int main() {
std::cout << "for por rango:\n";
for (int value : TraceRange(2)) {
std::cout << "valor " << value << "\n";
}
std::cout << "ciclo equivalente:\n";
TraceRange range(2);
auto it = range.begin();
auto last = range.end();
for (; it != last; ++it) {
int value = *it;
std::cout << "valor " << value << "\n";
}
return 0;
}// Tipo para las pruebas: cuenta cuántos objetos viven y cuántas copias y movimientos hubo.
#ifndef TRACKED_H
#define TRACKED_H
#include <utility>
class Tracked {
public:
struct Counts {
long live = 0;
long copies = 0;
long moves = 0;
};
static Counts& counts() {
static Counts values;
return values;
}
Tracked() { counts().live++; }
explicit Tracked(int value) : value_(value) { counts().live++; }
Tracked(const Tracked& other) : value_(other.value_) {
counts().live++;
counts().copies++;
}
Tracked(Tracked&& other) noexcept : value_(std::exchange(other.value_, 0)) {
counts().live++;
counts().moves++;
}
Tracked& operator=(const Tracked& other) {
value_ = other.value_;
counts().copies++;
return *this;
}
Tracked& operator=(Tracked&& other) noexcept {
value_ = std::exchange(other.value_, 0);
counts().moves++;
return *this;
}
~Tracked() { counts().live--; }
int value() const { return value_; }
bool operator==(const Tracked& other) const { return value_ == other.value_; }
private:
int value_ = 0;
};
#endif// Recorre n enteros sumándolos, con cuatro formas de guardarlos o de visitarlos.
// Compilar con -O2 y sin sanitizers. Los tiempos dependen de la máquina.
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <numeric>
#include <random>
#include <vector>
#include "bench.h"
#include "dlist.h"
namespace {
const int REPS = 5;
// Cada medición toca al menos esta cantidad de elementos, para que dure lo suficiente.
const std::size_t MIN_VISITS = 2000000;
const unsigned SEED = 2026;
template <typename Walk>
double nanos_per_element(std::size_t n, Walk walk) {
std::size_t passes = std::max<std::size_t>(1, MIN_VISITS / n);
double seconds = median_seconds(REPS, [&] {
for (std::size_t pass = 0; pass < passes; pass++) {
walk();
}
});
return seconds * 1e9 / static_cast<double>(passes * n);
}
// Porcentaje de saltos entre elementos seguidos que avanzan exactamente `gap` bytes.
double percent_adjacent(const DList<int>& list, std::ptrdiff_t gap) {
std::size_t hits = 0;
std::size_t hops = 0;
const int* previous = nullptr;
for (const int& value : list) {
if (previous != nullptr) {
auto jump = reinterpret_cast<std::intptr_t>(&value) - reinterpret_cast<std::intptr_t>(previous);
hits += jump == gap ? 1 : 0;
hops++;
}
previous = &value;
}
return 100.0 * static_cast<double>(hits) / static_cast<double>(hops);
}
void measure(std::size_t n) {
std::vector<int> values(n);
std::iota(values.begin(), values.end(), 0);
std::vector<std::size_t> order(n);
std::iota(order.begin(), order.end(), std::size_t{0});
std::mt19937 rng(SEED);
std::shuffle(order.begin(), order.end(), rng);
DList<int> list;
for (std::size_t i = 0; i < n; i++) {
list.push_back(values[i]);
}
long long expected = 0;
for (int value : values) {
expected += value;
}
auto sum_array = [&] {
long long sum = 0;
for (int value : values) {
sum += value;
}
keep(sum);
};
auto sum_array_shuffled = [&] {
long long sum = 0;
for (std::size_t i : order) {
sum += values[i];
}
keep(sum);
};
auto sum_list = [&] {
long long sum = 0;
for (int value : list) {
sum += value;
}
keep(sum);
};
double array_ns = nanos_per_element(n, sum_array);
double array_shuffled_ns = nanos_per_element(n, sum_array_shuffled);
double list_ns = nanos_per_element(n, sum_list);
double adjacent = percent_adjacent(list, 32);
list.shuffle_nodes(SEED);
list.check_invariants();
double list_shuffled_ns = nanos_per_element(n, sum_list);
double adjacent_shuffled = percent_adjacent(list, 32);
long long check = 0;
for (int value : list) {
check += value;
}
std::printf("%8zu %8.2f %9.2f %8.2f %9.2f %8.1f %9.1f %7.1f%% %7.1f%% %s\n",
n, array_ns, array_shuffled_ns, list_ns, list_shuffled_ns,
list_ns / array_ns, list_shuffled_ns / list_ns,
adjacent, adjacent_shuffled, check == expected ? "ok" : "MAL");
}
} // namespace
int main() {
std::printf("%8s %8s %9s %8s %9s %8s %9s %8s %8s\n",
"n", "arreglo", "arr.baraj", "lista", "lis.baraj",
"lis/arr", "lb/lista", "cont", "cont.b");
for (std::size_t n = 1024; n <= 4194304; n *= 4) {
measure(n);
}
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// Error a propósito: borrar el nodo actual dentro de un for por rango.
#include "../dlist.h"
int main() {
DList<int> list;
list.push_back(1);
list.push_back(2);
list.push_back(3);
auto it = list.begin();
for (int value : list) {
if (value == 1) {
list.erase(it); // el ++ oculto del for lee el next del nodo borrado
}
}
return 0;
}// Error a propósito: al insertar se olvida actualizar el prev del siguiente nodo.
// La lista se ve bien hacia adelante; check_invariants lo descubre.
#include <cassert>
#include <iostream>
class BrokenList {
struct Node {
int value = 0;
Node* prev = nullptr;
Node* next = nullptr;
};
public:
BrokenList() : sentinel_(new Node) {
sentinel_->prev = sentinel_;
sentinel_->next = sentinel_;
}
BrokenList(const BrokenList&) = delete;
BrokenList& operator=(const BrokenList&) = delete;
~BrokenList() {
Node* node = sentinel_->next;
while (node != sentinel_) {
Node* next = node->next;
delete node;
node = next;
}
delete sentinel_;
}
void push_front(int value) {
Node* first = sentinel_->next;
Node* node = new Node{value, sentinel_, first};
sentinel_->next = node;
// falta: first->prev = node;
}
void print() const {
for (const Node* node = sentinel_->next; node != sentinel_; node = node->next) {
std::cout << node->value << " ";
}
std::cout << std::endl;
}
void check_invariants() const {
const Node* node = sentinel_;
do {
assert(node->next->prev == node);
assert(node->prev->next == node);
node = node->next;
} while (node != sentinel_);
}
private:
Node* sentinel_;
};
int main() {
BrokenList list;
list.push_front(2);
list.push_front(1);
list.print();
list.check_invariants();
return 0;
}// Error a propósito: usar un iterador de un vector después de que creció.
#include <iostream>
#include <vector>
int main() {
std::vector<int> values = {1, 2, 3};
auto it = values.begin();
values.push_back(4); // la capacidad era 3: el bloque cambia de lugar
std::cout << *it << "\n";
return 0;
}// Error a propósito: el destructor lee node->next después de borrar node.
struct Node {
int value;
Node* next;
};
class BadList {
public:
BadList() = default;
BadList(const BadList&) = delete;
BadList& operator=(const BadList&) = delete;
~BadList() {
for (Node* node = head_; node != nullptr; node = node->next) {
delete node;
}
}
void push_front(int value) { head_ = new Node{value, head_}; }
private:
Node* head_ = nullptr;
};
int main() {
BadList list;
list.push_front(1);
list.push_front(2);
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Corre los ejemplos de la semana y comprueba lo que se puede contar: nunca tiempos absolutos."""
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
SAN = ["g++", "-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]
def run(*command):
return subprocess.run(
list(command), cwd=HERE, text=True, stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
class Examples(unittest.TestCase):
def test_list_tests_pass(self):
done = run("build/test_structures")
self.assertEqual(done.returncode, 0, done.stdout)
def test_programs_exit_cleanly(self):
for name in ["slist_demo", "dlist_demo", "trace_range", "invalidation", "erase_evens",
"same_interface", "alloc_count", "node_addresses"]:
with self.subTest(program=name):
done = run(f"build/{name}")
self.assertEqual(done.returncode, 0, done.stdout)
def test_allocations_are_counted(self):
lines = dict(line.split(": ") for line in run("build/alloc_count").stdout.splitlines()
if ": " in line)
self.assertEqual(int(lines["DList"]), 1000001)
self.assertEqual(int(lines["std::list"]), 1000000)
self.assertLess(int(lines["std::vector"]), 40)
def test_shuffle_really_scatters_the_nodes(self):
"""Los porcentajes de nodos seguidos son conteos, y las sumas deben coincidir."""
rows = run("build/traversal_bench").stdout.splitlines()[1:]
self.assertEqual(len(rows), 7)
for row in rows:
fields = row.split()
with self.subTest(n=fields[0]):
self.assertEqual(fields[-1], "ok")
self.assertGreater(float(fields[-3].rstrip("%")), 95.0)
self.assertLess(float(fields[-2].rstrip("%")), 5.0)
class DeliberateErrors(unittest.TestCase):
def build(self, name):
binary = f"build/{name}"
done = run(*SAN, f"errors/{name}.cpp", "-o", binary)
self.assertEqual(done.returncode, 0, done.stdout)
return run(binary)
def test_use_after_delete(self):
done = self.build("use_after_delete")
self.assertNotEqual(done.returncode, 0)
self.assertIn("heap-use-after-free", done.stdout)
self.assertIn("errors/use_after_delete.cpp:14", done.stdout)
def test_erase_in_range_for(self):
done = self.build("erase_in_range_for")
self.assertNotEqual(done.returncode, 0)
self.assertIn("heap-use-after-free", done.stdout)
self.assertRegex(done.stdout, re.compile(r"operator\+\+"))
def test_stale_vector_iterator(self):
done = self.build("stale_vector_iterator")
self.assertNotEqual(done.returncode, 0)
self.assertIn("heap-use-after-free", done.stdout)
def test_forgot_prev_is_found_by_invariants(self):
done = self.build("forgot_prev")
self.assertNotEqual(done.returncode, 0)
self.assertIn("1 2", done.stdout)
self.assertIn("check_invariants", done.stdout)
self.assertNotIn("AddressSanitizer", done.stdout)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde están los deques: lab o solution.
Cada prueba corre en su propio proceso, con sanitizers: una fuga o un acceso inválido
la hace fallar aunque el resultado parezca correcto. Las dos representaciones pasan
por las mismas pruebas comunes, escritas como templates en lab/tests/deque_suite.h.
"""
import os
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
BINARY = HERE / "build" / LAB_DIR / "deque_tests"
COMMON = [
"new_is_empty", "push_back_keeps_order", "push_front_reverses", "as_stack", "as_queue",
"growth_while_wrapped", "index_reads_and_writes", "range_for", "iterator_operators",
"iterator_after_wrap", "copy_is_independent", "self_assignment",
"move_leaves_empty_and_usable", "no_objects_left_behind", "rvalue_push_does_not_copy",
"lvalue_push_copies_once", "push_own_element", "pop_releases_value", "random_ints",
"random_strings",
]
ONLY_ARRAY = ["few_allocations", "capacity_doubles", "ring_reuses_space", "move_is_noexcept"]
ONLY_LINKED = ["one_allocation_per_push", "elements_stay_in_place", "move_assign_is_noexcept",
"shuffle_nodes"]
def short_report(text):
"""Deja lo útil de un informe: el tipo de error y las líneas de sus archivos."""
if "ERROR:" not in text and "runtime error" not in text:
return text.strip()
useful = [line.strip() for line in text.splitlines()
if "ERROR:" in line or "SUMMARY:" in line or "runtime error" in line
or re.search(r"(array_deque|linked_deque|deque_suite)\.h:\d+", line)]
return "\n".join(useful[:12])
def build():
done = subprocess.run(
["make", "--no-print-directory", f"LAB_DIR={LAB_DIR}", f"build/{LAB_DIR}/deque_tests"],
cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
return done.returncode, done.stdout
def run_case(testcase, kind, name):
done = subprocess.run(
[str(BINARY), kind, name], cwd=HERE, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
testcase.assertEqual(done.returncode, 0, short_report(done.stdout))
def add_cases(cls, kind, names):
for name in names:
def case(self, name=name):
run_case(self, kind, name)
setattr(cls, f"test_{name}", case)
return cls
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class Builds(unittest.TestCase):
def test_builds_without_warnings(self):
BINARY.unlink(missing_ok=True)
code, output = build()
self.assertEqual(code, 0, output)
self.assertNotIn("warning", output)
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestArrayDeque(unittest.TestCase):
@classmethod
def setUpClass(cls):
code, output = build()
if code != 0:
raise AssertionError(output)
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestLinkedDeque(unittest.TestCase):
@classmethod
def setUpClass(cls):
code, output = build()
if code != 0:
raise AssertionError(output)
add_cases(TestArrayDeque, "array", COMMON + ONLY_ARRAY)
add_cases(TestLinkedDeque, "linked", COMMON + ONLY_LINKED)
if __name__ == "__main__":
unittest.main()"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import os
import pathlib
import platform
import re
import subprocess
import unittest
try:
import pty
except ImportError:
pty = None
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-05.md"
SLIDES = ROOT / "slides" / "week-05.tex"
# Antes de un bloque cuya salida cambia con el sistema. Se corre, pero no se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un informe de ASan. Puede omitir líneas, pero no cambiarlas ni reordenarlas.
# Las direcciones y los números de proceso no se comparan.
REPORT = "<!-- informe recortado -->"
# Los informes se capturaron en esta arquitectura: en otra cambian líneas de las bibliotecas.
REPORT_MACHINE = "aarch64"
def normalize(text):
# Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
return text.replace("‘", "'").replace("’", "'")
def run_shell(command):
done = subprocess.run(
["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8",
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
return normalize(done.stdout)
def run_in_terminal(command):
"""Corre el comando en una terminal de verdad: así se intercala la salida del programa
con el informe, igual que cuando ustedes lo corren."""
master, slave = pty.openpty()
process = subprocess.Popen(
["bash", "-c", command], cwd=HERE, stdin=subprocess.DEVNULL, stdout=slave, stderr=slave,
)
os.close(slave)
data = b""
while True:
try:
chunk = os.read(master, 4096)
except OSError:
break
if not chunk:
break
data += chunk
os.close(master)
process.wait(timeout=120)
text = data.decode("utf-8", errors="replace").replace("\r", "")
return normalize(re.sub(r"\x1b\[[0-9;]*[mK]", "", text))
def clean_lines(text):
return [line.rstrip() for line in text.splitlines()]
def stable(line):
"""Quita de una línea de informe lo que cambia en cada corrida."""
line = re.sub(r"0x[0-9a-f]+", "ADDR", line.strip())
return re.sub(r"==\d+==", "==N==", line)
def split_commands(lines):
"""Parte un bloque de terminal en pares (comando, líneas de salida)."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
return pairs
def note_blocks(language):
"""Devuelve (líneas, marca) de cada bloque de los apuntes."""
blocks, current, mark, previous = [], None, "", ""
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, mark = [], previous.strip()
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, mark))
current = None
else:
current.append(line)
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def known_code_lines():
lines = set()
sources = list(HERE.glob("*.cpp")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.cpp"))
sources += list((HERE / "lab").glob("*.h")) + list((HERE / "lab" / "tests").glob("*"))
for source in sources:
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
class ReportCheck(unittest.TestCase):
def assert_in_order(self, command, expected, actual):
"""Las líneas esperadas aparecen en la salida real, en orden. '...' marca un recorte."""
wanted = [stable(line) for line in expected if line.strip() and line.strip() != "..."]
real = [stable(line) for line in actual]
position = 0
for line in wanted:
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(real)) if real[i] == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
class NotesAreTrue(ReportCheck):
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ ") or mark == REPORT:
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_shell(command))
checked += 1
if mark != SYSTEM_DEPENDENT:
with self.subTest(command=command):
self.assertEqual(actual, expected)
self.assertGreater(checked, 5)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
@unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
def test_reports_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if mark != REPORT:
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_in_terminal(command))
self.assert_in_order(command, expected, actual)
checked += 1
self.assertGreater(checked, 4)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for lines, _ in note_blocks("cpp"):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
class SlidesAreTrue(ReportCheck):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_program_output_in_slides(self):
for block in slide_blocks("terminal|salida"):
for command, expected in split_commands(block):
self.assert_in_order(command, expected, clean_lines(run_shell(command)))
# Algunos informes usan programas que compiló la prueba de los apuntes, que corre antes.
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
@unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
def test_reports_in_slides(self):
for block in slide_blocks("informe"):
for command, expected in split_commands(block):
self.assert_in_order(command, expected, clean_lines(run_in_terminal(command)))
@unittest.skipUnless(NOTES.exists() and SLIDES.exists(), "faltan los apuntes o las diapositivas")
def test_measurements_in_slides_come_from_notes(self):
"""Los tiempos dependen de la máquina: las diapositivas copian los de los apuntes."""
notes = NOTES.read_text(encoding="utf-8").splitlines()
blocks = slide_blocks("medicion")
self.assertGreater(len(blocks), 2)
for block in blocks:
for line in block:
with self.subTest(line=line):
self.assertIn(line, notes)
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_code_blocks_in_slides_exist(self):
known = known_code_lines()
for block in slide_blocks("codigo|codigochico|codigomini"):
for line in block:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
if __name__ == "__main__":
unittest.main()// Pruebas de las listas de los apuntes: casos dirigidos y operaciones al azar con semilla fija.
#include <algorithm>
#include <cassert>
#include <iostream>
#include <list>
#include <random>
#include <string>
#include <vector>
#include "dlist.h"
#include "slist.h"
#include "tracked.h"
namespace {
template <typename T>
std::vector<T> as_vector(const DList<T>& list) {
std::vector<T> values;
for (const T& value : list) {
values.push_back(value);
}
return values;
}
void test_singly_list() {
SinglyList<int> list;
assert(list.empty());
list.check_invariants();
list.push_front(2);
list.push_front(1);
list.push_back(3);
list.check_invariants();
assert(list.size() == 3);
assert(list.at(0) == 1);
assert(list.at(1) == 2);
assert(list.at(2) == 3);
list.pop_front();
list.pop_front();
list.pop_front();
list.check_invariants();
assert(list.empty());
list.push_back(9); // al vaciarse, tail_ debió volver a nulo
assert(list.front() == 9);
list.check_invariants();
}
void test_singly_list_frees_everything() {
{
SinglyList<Tracked> list;
for (int i = 0; i < 100; i++) {
list.push_front(Tracked(i));
}
assert(Tracked::counts().live == 100);
}
assert(Tracked::counts().live == 0);
}
void test_dlist_empty() {
DList<int> list;
assert(list.empty());
assert(!(list.begin() != list.end()));
list.check_invariants();
}
void test_dlist_insert_erase() {
DList<int> list;
list.push_back(1);
list.push_back(3);
auto it = list.begin();
++it;
auto two = list.insert(it, 2);
assert(*two == 2);
assert(as_vector(list) == (std::vector<int>{1, 2, 3}));
auto three = list.erase(two);
assert(*three == 3);
assert(as_vector(list) == (std::vector<int>{1, 3}));
list.check_invariants();
}
void test_iterator_survives_other_changes() {
DList<int> list;
list.push_back(5);
auto it = list.begin();
int* element = &*it;
for (int i = 0; i < 100; i++) {
list.push_front(i);
list.push_back(i);
}
assert(*it == 5);
assert(&*it == element);
list.erase(list.begin());
list.pop_back();
assert(*it == 5);
list.check_invariants();
}
void test_write_through_iterator() {
DList<int> list;
list.push_back(1);
list.push_back(2);
for (int& value : list) {
value *= 10;
}
const DList<int>& view = list;
assert(as_vector(view) == (std::vector<int>{10, 20}));
}
void test_shuffle_keeps_nodes() {
DList<int> list;
std::vector<const int*> before;
for (int i = 0; i < 1000; i++) {
list.push_back(i);
}
for (const int& value : list) {
before.push_back(&value);
}
list.shuffle_nodes(7);
list.check_invariants();
std::vector<const int*> after;
long long sum = 0;
for (const int& value : list) {
after.push_back(&value);
sum += value;
}
assert(sum == 999 * 1000 / 2);
assert(after != before);
std::sort(before.begin(), before.end());
std::sort(after.begin(), after.end());
assert(after == before);
}
void test_dlist_frees_everything() {
{
DList<Tracked> list;
for (int i = 0; i < 100; i++) {
list.push_back(Tracked(i));
}
assert(Tracked::counts().live == 101); // 100 elementos y el centinela
list.erase(list.begin());
}
assert(Tracked::counts().live == 0);
}
// Operaciones al azar, comparando contra std::list.
void test_dlist_random() {
std::mt19937 rng(2026);
DList<std::string> mine;
std::list<std::string> model;
for (int step = 0; step < 20000; step++) {
std::string value = std::to_string(rng() % 1000);
switch (rng() % 6) {
case 0:
mine.push_front(value);
model.push_front(value);
break;
case 1:
mine.push_back(value);
model.push_back(value);
break;
case 2:
if (!model.empty()) {
mine.pop_front();
model.pop_front();
}
break;
case 3:
if (!model.empty()) {
mine.pop_back();
model.pop_back();
}
break;
case 4: {
auto a = mine.begin();
auto b = model.begin();
for (unsigned skip = rng() % (model.size() + 1); skip > 0; skip--) {
++a;
++b;
}
auto inserted_a = mine.insert(a, value);
auto inserted_b = model.insert(b, value);
assert(*inserted_a == *inserted_b);
break;
}
default:
if (!model.empty()) {
auto a = mine.begin();
auto b = model.begin();
for (unsigned skip = rng() % model.size(); skip > 0; skip--) {
++a;
++b;
}
mine.erase(a);
model.erase(b);
}
}
assert(mine.size() == model.size());
if (step % 100 == 0) {
mine.check_invariants();
std::vector<std::string> expected(model.begin(), model.end());
assert(as_vector(mine) == expected);
}
}
}
} // namespace
int main() {
test_singly_list();
test_singly_list_frees_everything();
test_dlist_empty();
test_dlist_insert_erase();
test_iterator_survives_other_changes();
test_write_through_iterator();
test_shuffle_keeps_nodes();
test_dlist_frees_everything();
test_dlist_random();
std::cout << "pruebas de las listas: ok\n";
return 0;
}Laboratorio
Archivos de partida del laboratorio de la semana.
// Deque sobre un buffer circular que crece. Completen cada método marcado con not_implemented.
#ifndef ARRAY_DEQUE_H
#define ARRAY_DEQUE_H
#include <cassert>
#include <cstddef>
#include <utility>
#include "not_implemented.h"
// Invariantes:
// - capacity_ == 0 si y solo si data_ es nulo.
// - size_ <= capacity_.
// - Si capacity_ > 0, entonces front_ < capacity_. Si capacity_ == 0, entonces front_ == 0.
// - El elemento i está en data_[(front_ + i) mod capacity_].
//
// T debe tener constructor por defecto, porque el buffer se reserva con new T[]: las casillas
// sin elemento también guardan un T.
template <typename T>
class ArrayDeque {
public:
// Con V = T se puede escribir el elemento; con V = const T, solo leerlo.
// Un iterador guarda el índice lógico: 0 es el frente y size_ es el fin.
template <typename V>
class Iter {
public:
V& operator*() const { not_implemented("Iter::operator*"); }
Iter& operator++() { not_implemented("Iter::operator++"); }
bool operator==(const Iter& other) const { return data_ == other.data_ && index_ == other.index_; }
bool operator!=(const Iter& other) const { return !(*this == other); }
private:
friend class ArrayDeque;
Iter(V* data, std::size_t capacity, std::size_t front, std::size_t index)
: data_(data), capacity_(capacity), front_(front), index_(index) {}
V* data_;
std::size_t capacity_;
std::size_t front_;
std::size_t index_;
};
using iterator = Iter<T>;
using const_iterator = Iter<const T>;
ArrayDeque() { not_implemented("ArrayDeque()"); }
~ArrayDeque() { not_implemented("~ArrayDeque"); }
ArrayDeque(const ArrayDeque&) { not_implemented("constructor de copia"); }
ArrayDeque& operator=(const ArrayDeque&) { not_implemented("asignación de copia"); }
// Al mover, el origen queda vacío y se puede seguir usando.
ArrayDeque(ArrayDeque&&) noexcept { not_implemented("constructor de movimiento"); }
ArrayDeque& operator=(ArrayDeque&&) noexcept { not_implemented("asignación de movimiento"); }
std::size_t size() const { return size_; }
bool empty() const { return size_ == 0; }
std::size_t capacity() const { return capacity_; }
void push_front(const T&) { not_implemented("push_front(const T&)"); }
void push_front(T&&) { not_implemented("push_front(T&&)"); }
void push_back(const T&) { not_implemented("push_back(const T&)"); }
void push_back(T&&) { not_implemented("push_back(T&&)"); }
// Precondición: no está vacío. La casilla que queda libre debe soltar lo que guardaba.
void pop_front() { not_implemented("pop_front"); }
void pop_back() { not_implemented("pop_back"); }
T& front() { not_implemented("front"); }
const T& front() const { not_implemented("front const"); }
T& back() { not_implemented("back"); }
const T& back() const { not_implemented("back const"); }
// Precondición: i < size().
T& operator[](std::size_t) { not_implemented("operator[]"); }
const T& operator[](std::size_t) const { not_implemented("operator[] const"); }
iterator begin() { not_implemented("begin"); }
iterator end() { not_implemented("end"); }
const_iterator begin() const { not_implemented("begin const"); }
const_iterator end() const { not_implemented("end const"); }
void check_invariants() const {
assert((capacity_ == 0) == (data_ == nullptr));
assert(size_ <= capacity_);
if (capacity_ == 0) {
assert(front_ == 0);
} else {
assert(front_ < capacity_);
}
}
private:
T* data_ = nullptr;
std::size_t capacity_ = 0;
std::size_t front_ = 0;
std::size_t size_ = 0;
};
#endif# Conclusión del laboratorio
Peguen aquí la tabla de `make bench` y respondan con sus propios números.
## Cómo midieron
Máquina, cómo compilaron, cuántas repeticiones y cuántas corridas.
## Tabla
## Operaciones en los extremos
Las dos representaciones son O(1) (una de ellas, amortizado). ¿Cuál es más rápida en su máquina,
por cuánto, y qué conteo de la tabla ayuda a explicarlo?
## Recorrido
Las dos son Θ(n). ¿Por qué los tiempos no son iguales? ¿Qué cambia al barajar los nodos, si el código
del recorrido es el mismo? ¿Qué parte de su explicación midieron y qué parte supusieron?// Deque sobre una lista doble con centinela. Completen cada método marcado con not_implemented.
#ifndef LINKED_DEQUE_H
#define LINKED_DEQUE_H
#include <cassert>
#include <cstddef>
#include <utility>
#include "not_implemented.h"
// Invariantes:
// - sentinel_ nunca es nulo, y su value no es un elemento del deque.
// - Los nodos forman un ciclo: para todo nodo x, x->next->prev == x y x->prev->next == x.
// - Siguiendo next desde sentinel_ se visitan size_ nodos antes de volver a sentinel_.
//
// T debe tener constructor por defecto, porque el centinela también guarda un T.
template <typename T>
class LinkedDeque {
struct Node {
T value{};
Node* prev = nullptr;
Node* next = nullptr;
};
public:
// Con V = T se puede escribir el elemento; con V = const T, solo leerlo.
template <typename V>
class Iter {
public:
V& operator*() const { not_implemented("Iter::operator*"); }
Iter& operator++() { not_implemented("Iter::operator++"); }
bool operator==(const Iter& other) const { return node_ == other.node_; }
bool operator!=(const Iter& other) const { return node_ != other.node_; }
private:
friend class LinkedDeque;
explicit Iter(Node* node) : node_(node) {}
Node* node_;
};
using iterator = Iter<T>;
using const_iterator = Iter<const T>;
LinkedDeque() { not_implemented("LinkedDeque()"); }
~LinkedDeque() { not_implemented("~LinkedDeque"); }
LinkedDeque(const LinkedDeque&) { not_implemented("constructor de copia"); }
LinkedDeque& operator=(const LinkedDeque&) { not_implemented("asignación de copia"); }
// Al mover, el origen queda vacío y se puede seguir usando: también necesita un centinela.
// Por eso este constructor reserva memoria y no es noexcept.
LinkedDeque(LinkedDeque&&) { not_implemented("constructor de movimiento"); }
LinkedDeque& operator=(LinkedDeque&&) noexcept { not_implemented("asignación de movimiento"); }
std::size_t size() const { return size_; }
bool empty() const { return size_ == 0; }
void push_front(const T&) { not_implemented("push_front(const T&)"); }
void push_front(T&&) { not_implemented("push_front(T&&)"); }
void push_back(const T&) { not_implemented("push_back(const T&)"); }
void push_back(T&&) { not_implemented("push_back(T&&)"); }
// Precondición: no está vacío.
void pop_front() { not_implemented("pop_front"); }
void pop_back() { not_implemented("pop_back"); }
T& front() { not_implemented("front"); }
const T& front() const { not_implemented("front const"); }
T& back() { not_implemented("back"); }
const T& back() const { not_implemented("back const"); }
// Precondición: i < size(). Parte del extremo más cercano.
T& operator[](std::size_t) { not_implemented("operator[]"); }
const T& operator[](std::size_t) const { not_implemented("operator[] const"); }
iterator begin() { not_implemented("begin"); }
iterator end() { not_implemented("end"); }
const_iterator begin() const { not_implemented("begin const"); }
const_iterator end() const { not_implemented("end const"); }
// Vuelve a enlazar los nodos en orden aleatorio, sin moverlos en la memoria.
// Los elementos quedan en otro orden. Solo sirve para medir.
void shuffle_nodes(unsigned) { not_implemented("shuffle_nodes"); }
void check_invariants() const {
assert(sentinel_ != nullptr);
std::size_t count = 0;
const Node* node = sentinel_;
do {
assert(node->next->prev == node);
assert(node->prev->next == node);
node = node->next;
count++;
assert(count <= size_ + 1);
} while (node != sentinel_);
assert(count == size_ + 1);
}
private:
Node* sentinel_ = nullptr;
std::size_t size_ = 0;
};
#endif// Para los métodos que todavía no escriben: detiene el programa y dice cuál falta.
#ifndef NOT_IMPLEMENTED_H
#define NOT_IMPLEMENTED_H
#include <cstdio>
#include <cstdlib>
[[noreturn]] inline void not_implemented(const char* name) {
std::fprintf(stderr, "sin implementar: %s\n", name);
std::abort();
}
#endif// Mide las dos representaciones del deque con el mismo código.
// Se compila con -O2 y sin sanitizers. Los tiempos dependen de la máquina.
#include <cstddef>
#include <cstdio>
#include <vector>
#include "alloc_counter.h"
#include "array_deque.h"
#include "bench.h"
#include "linked_deque.h"
namespace {
const int REPS = 5;
const std::size_t MIN_VISITS = 2000000;
const unsigned SEED = 2026;
struct Row {
double ends_ns;
double sum_ns;
double sum_shuffled_ns;
std::size_t allocations;
};
double per_element(double seconds, std::size_t count) {
return seconds * 1e9 / static_cast<double>(count);
}
// Insertar n por detrás y por delante, y sacarlos: nanosegundos por operación.
template <typename D>
double measure_ends(std::size_t n) {
double seconds = median_seconds(REPS, [&] {
D deque;
for (std::size_t i = 0; i < n; i++) {
deque.push_back(static_cast<int>(i));
deque.push_front(static_cast<int>(i));
}
for (std::size_t i = 0; i < n; i++) {
keep(deque.front());
deque.pop_front();
keep(deque.back());
deque.pop_back();
}
});
return per_element(seconds, 4 * n);
}
template <typename D>
double measure_sum(const D& deque) {
std::size_t n = deque.size();
std::size_t passes = MIN_VISITS / n > 0 ? MIN_VISITS / n : 1;
double seconds = median_seconds(REPS, [&] {
for (std::size_t pass = 0; pass < passes; pass++) {
long long sum = 0;
for (int value : deque) {
sum += value;
}
keep(sum);
}
});
return per_element(seconds, passes * n);
}
template <typename D>
D filled(std::size_t n) {
D deque;
for (std::size_t i = 0; i < n; i++) {
deque.push_back(static_cast<int>(i));
}
return deque;
}
template <typename D>
std::size_t count_allocations(std::size_t n) {
std::size_t before = allocation_count();
D deque = filled<D>(n);
return allocation_count() - before;
}
} // namespace
int main() {
std::printf("Tiempos en ns: por operación en los extremos, por elemento al recorrer.\n");
std::printf("%8s | %8s %8s %9s | %8s %8s %9s %9s\n", "n", "arr.ext", "arr.rec", "arr.res",
"lis.ext", "lis.rec", "lis.baraj", "lis.res");
for (std::size_t n = 4096; n <= 4194304; n *= 16) {
using Array = ArrayDeque<int>;
using Linked = LinkedDeque<int>;
Array array_deque = filled<Array>(n);
double array_sum = measure_sum(array_deque);
double array_ends = measure_ends<Array>(n);
double linked_ends = measure_ends<Linked>(n);
Linked linked_deque = filled<Linked>(n);
double linked_sum = measure_sum(linked_deque);
linked_deque.shuffle_nodes(SEED);
double linked_shuffled = measure_sum(linked_deque);
std::printf("%8zu | %8.2f %8.2f %9zu | %8.2f %8.2f %9.2f %9zu\n", n, array_ends, array_sum,
count_allocations<Array>(n), linked_ends, linked_sum, linked_shuffled,
count_allocations<Linked>(n));
}
return 0;
}// Pruebas públicas del laboratorio. Cada una es un template: D es ArrayDeque o LinkedDeque.
// No hay clase base: la interfaz común es lo que estas pruebas usan.
#ifndef DEQUE_SUITE_H
#define DEQUE_SUITE_H
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <deque>
#include <memory>
#include <random>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "alloc_counter.h"
#include "tracked.h"
template <typename D, typename T>
void assert_same(const D& mine, const std::deque<T>& model) {
mine.check_invariants();
assert(mine.size() == model.size());
assert(mine.empty() == model.empty());
if (!model.empty()) {
assert(mine.front() == model.front());
assert(mine.back() == model.back());
}
auto expected = model.begin();
for (const T& value : mine) {
assert(expected != model.end());
assert(value == *expected);
++expected;
}
assert(expected == model.end());
}
template <typename D>
void test_new_is_empty() {
D deque;
assert(deque.size() == 0);
assert(deque.empty());
deque.check_invariants();
}
template <typename D>
void test_push_back_keeps_order() {
D deque;
for (int i = 0; i < 100; i++) {
deque.push_back(i);
assert(deque.front() == 0);
assert(deque.back() == i);
assert(deque.size() == static_cast<std::size_t>(i) + 1);
}
deque.check_invariants();
}
template <typename D>
void test_push_front_reverses() {
D deque;
for (int i = 0; i < 100; i++) {
deque.push_front(i);
assert(deque.front() == i);
assert(deque.back() == 0);
}
for (int i = 0; i < 100; i++) {
assert(deque[i] == 99 - i);
}
deque.check_invariants();
}
template <typename D>
void test_as_stack() {
D deque;
for (int i = 0; i < 50; i++) {
deque.push_back(i);
}
for (int i = 49; i >= 0; i--) {
assert(deque.back() == i);
deque.pop_back();
}
assert(deque.empty());
deque.check_invariants();
}
template <typename D>
void test_as_queue() {
D deque;
for (int i = 0; i < 50; i++) {
deque.push_back(i);
}
for (int i = 0; i < 50; i++) {
assert(deque.front() == i);
deque.pop_front();
}
assert(deque.empty());
deque.check_invariants();
}
// El frente da vueltas por el buffer y además crece: aquí se nota si el buffer se desenrolla mal.
template <typename D>
void test_growth_while_wrapped() {
D deque;
std::deque<int> model;
int next = 0;
for (int round = 0; round < 200; round++) {
for (int i = 0; i < 3; i++) {
deque.push_back(next);
model.push_back(next);
next++;
}
deque.push_front(next);
model.push_front(next);
next++;
deque.pop_back();
model.pop_back();
deque.pop_front();
model.pop_front();
assert_same(deque, model);
}
}
template <typename D>
void test_index_reads_and_writes() {
D deque;
for (int i = 0; i < 20; i++) {
deque.push_back(i);
}
for (int i = 0; i < 5; i++) {
deque.pop_front();
deque.push_back(100 + i);
}
deque[0] = -1;
deque[19] = -2;
const D& view = deque;
assert(view[0] == -1);
assert(view[1] == 6);
assert(view[15] == 100);
assert(view[19] == -2);
}
template <typename D>
void test_range_for() {
D deque;
int sum = 0;
for (int value : deque) {
sum += value;
}
assert(sum == 0);
for (int i = 1; i <= 10; i++) {
deque.push_back(i);
}
for (int& value : deque) {
value *= 2;
}
const D& view = deque;
for (const int& value : view) {
sum += value;
}
assert(sum == 110);
}
template <typename D>
void test_iterator_operators() {
D deque;
assert(!(deque.begin() != deque.end()));
assert(deque.begin() == deque.end());
deque.push_back(1);
deque.push_back(2);
deque.push_back(3);
auto it = deque.begin();
assert(it != deque.end());
assert(*it == 1);
assert(*++it == 2);
*it = 20;
assert(deque[1] == 20);
++(++it);
assert(it == deque.end());
}
template <typename D>
void test_iterator_after_wrap() {
D deque;
for (int i = 0; i < 4; i++) {
deque.push_back(i);
}
deque.pop_front();
deque.pop_front();
deque.push_back(4);
deque.push_back(5);
deque.push_front(1);
int expected = 1;
for (int value : deque) {
assert(value == expected);
expected++;
}
assert(expected == 6);
}
template <typename D>
void test_copy_is_independent() {
D first;
for (int i = 0; i < 20; i++) {
first.push_back(i);
}
first.pop_front();
first.push_front(100);
D second(first);
assert(second.size() == first.size());
second[0] = -1;
second.push_back(-2);
assert(first[0] == 100);
assert(first.size() == 20);
first.check_invariants();
second.check_invariants();
D third;
third.push_back(7);
third = first;
third[1] = -3;
assert(first[1] == 1);
assert(third.size() == 20);
third.check_invariants();
}
template <typename D>
void test_self_assignment() {
D deque;
deque.push_back(1);
deque.push_back(2);
D& same = deque;
deque = same;
assert(deque.size() == 2);
assert(deque.front() == 1);
assert(deque.back() == 2);
deque.check_invariants();
}
template <typename D>
void test_move_leaves_empty_and_usable() {
D first;
for (int i = 0; i < 20; i++) {
first.push_back(i);
}
D second(std::move(first));
assert(second.size() == 20);
assert(second.back() == 19);
assert(first.empty());
first.check_invariants();
first.push_back(5);
first.push_front(4);
assert(first.front() == 4);
assert(first.back() == 5);
first.pop_back();
first.pop_back();
D third;
third.push_back(1);
third = std::move(second);
assert(third.size() == 20);
assert(second.empty());
second.check_invariants();
second.push_front(9);
assert(second.back() == 9);
third.check_invariants();
}
template <typename D>
void test_no_objects_left_behind() {
{
D first;
for (int i = 0; i < 100; i++) {
first.push_back(Tracked(i));
first.push_front(Tracked(-i));
}
for (int i = 0; i < 30; i++) {
first.pop_back();
first.pop_front();
}
D second(first);
D third;
third = second;
D fourth(std::move(second));
third = std::move(fourth);
}
assert(Tracked::counts().live == 0);
}
// Insertar un temporal no copia nunca, ni siquiera cuando el buffer crece.
template <typename D>
void test_rvalue_push_does_not_copy() {
D deque;
Tracked::counts().copies = 0;
for (int i = 0; i < 1000; i++) {
deque.push_back(Tracked(i));
deque.push_front(Tracked(i));
}
assert(Tracked::counts().copies == 0);
}
template <typename D>
void test_lvalue_push_copies_once() {
D deque;
Tracked value(1);
Tracked::counts().copies = 0;
for (int i = 0; i < 1000; i++) {
deque.push_back(value);
deque.push_front(value);
}
assert(Tracked::counts().copies == 2000);
}
// El argumento puede ser un elemento del mismo deque, aunque el buffer tenga que crecer.
template <typename D>
void test_push_own_element() {
// Por separado: así el crecimiento le toca a cada extremo.
D by_back;
D by_front;
by_back.push_back(std::string(40, 'a'));
by_front.push_back(std::string(40, 'a'));
for (int i = 0; i < 200; i++) {
by_back.push_back(by_back.front());
by_front.push_front(by_front.back());
}
assert(by_back.size() == 201);
assert(by_front.size() == 201);
for (const std::string& value : by_back) {
assert(value == std::string(40, 'a'));
}
for (const std::string& value : by_front) {
assert(value == std::string(40, 'a'));
}
}
// Sacar un elemento debe soltar lo que guardaba.
template <typename D>
void test_pop_releases_value() {
auto shared = std::make_shared<int>(1);
D deque;
deque.push_back(shared);
deque.push_front(shared);
assert(shared.use_count() == 3);
deque.pop_front();
assert(shared.use_count() == 2);
deque.pop_back();
assert(shared.use_count() == 1);
}
template <typename D, typename T, typename Make>
void random_against_std_deque(unsigned seed, int steps, Make make) {
std::mt19937 rng(seed);
D mine;
std::deque<T> model;
for (int step = 0; step < steps; step++) {
T value = make(rng());
switch (rng() % 8) {
case 0:
case 1:
mine.push_back(value);
model.push_back(value);
break;
case 2:
case 3:
mine.push_front(value);
model.push_front(value);
break;
case 4:
if (!model.empty()) {
mine.pop_back();
model.pop_back();
}
break;
case 5:
if (!model.empty()) {
mine.pop_front();
model.pop_front();
}
break;
case 6:
if (!model.empty()) {
std::size_t i = rng() % model.size();
assert(mine[i] == model[i]);
mine[i] = value;
model[i] = value;
}
break;
default:
if (step % 50 == 0) {
D copy(mine);
mine = std::move(copy);
}
}
assert(mine.size() == model.size());
if (step % 200 == 0) {
assert_same(mine, model);
}
}
assert_same(mine, model);
}
template <typename ArrayInt>
void test_array_few_allocations() {
std::size_t before = allocation_count();
{
ArrayInt deque;
for (int i = 0; i < 100000; i++) {
deque.push_back(i);
}
}
assert(allocation_count() - before <= 25);
}
template <typename ArrayInt>
void test_array_capacity_doubles() {
ArrayInt deque;
std::size_t last = deque.capacity();
for (int i = 0; i < 5000; i++) {
deque.push_back(i);
std::size_t now = deque.capacity();
if (last > 0 && now != last) {
assert(now == 2 * last);
}
last = now;
}
assert(last <= 2 * 5000);
}
// Una cola de pocos elementos debe reusar el buffer: si no da la vuelta, la capacidad crece.
template <typename ArrayInt>
void test_array_ring_reuses_space() {
ArrayInt deque;
for (int i = 0; i < 4; i++) {
deque.push_back(i);
}
std::size_t capacity = deque.capacity();
for (int i = 0; i < 100000; i++) {
deque.push_back(i);
deque.pop_front();
}
assert(deque.capacity() <= 2 * capacity);
deque.check_invariants();
}
template <typename ArrayInt>
void test_array_move_is_noexcept() {
static_assert(std::is_nothrow_move_constructible_v<ArrayInt>);
static_assert(std::is_nothrow_move_assignable_v<ArrayInt>);
}
template <typename LinkedInt>
void test_linked_one_allocation_per_push() {
LinkedInt deque;
std::size_t before = allocation_count();
for (int i = 0; i < 1000; i++) {
deque.push_back(i);
}
assert(allocation_count() - before == 1000);
before = allocation_count();
for (int i = 0; i < 1000; i++) {
deque.pop_front();
}
assert(allocation_count() == before);
}
template <typename LinkedInt>
void test_linked_elements_stay_in_place() {
LinkedInt deque;
deque.push_back(5);
int* element = &deque.front();
auto it = deque.begin();
for (int i = 0; i < 1000; i++) {
deque.push_back(i);
deque.push_front(i);
}
assert(&*it == element);
assert(*it == 5);
for (int i = 0; i < 1000; i++) {
deque.pop_back();
deque.pop_front();
}
assert(&deque.front() == element);
}
template <typename LinkedInt>
void test_linked_move_assign_is_noexcept() {
static_assert(std::is_nothrow_move_assignable_v<LinkedInt>);
}
template <typename LinkedInt>
void test_linked_shuffle_nodes() {
LinkedInt deque;
std::vector<const int*> before;
for (int i = 0; i < 1000; i++) {
deque.push_back(i);
}
for (const int& value : deque) {
before.push_back(&value);
}
deque.shuffle_nodes(7);
deque.check_invariants();
assert(deque.size() == 1000);
std::vector<const int*> after;
std::vector<int> values;
for (const int& value : deque) {
after.push_back(&value);
values.push_back(value);
assert(*after.back() == value);
}
assert(after != before);
std::vector<const int*> sorted_before = before;
std::sort(sorted_before.begin(), sorted_before.end());
std::vector<const int*> sorted_after = after;
std::sort(sorted_after.begin(), sorted_after.end());
assert(sorted_after == sorted_before);
std::sort(values.begin(), values.end());
for (int i = 0; i < 1000; i++) {
assert(values[i] == i);
}
deque.push_back(-1);
deque.push_front(-2);
assert(deque.back() == -1);
assert(deque.front() == -2);
deque.check_invariants();
}
#endif// Corre las pruebas: deque_tests array, deque_tests linked, o con el nombre de una sola prueba.
// Las dos representaciones pasan por la misma lista de pruebas comunes.
#include <cstring>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include "array_deque.h"
#include "deque_suite.h"
#include "linked_deque.h"
namespace {
struct Test {
const char* name;
void (*run)();
};
template <template <typename> class Deque>
std::vector<Test> common_tests() {
using Ints = Deque<int>;
return {
{"new_is_empty", test_new_is_empty<Ints>},
{"push_back_keeps_order", test_push_back_keeps_order<Ints>},
{"push_front_reverses", test_push_front_reverses<Ints>},
{"as_stack", test_as_stack<Ints>},
{"as_queue", test_as_queue<Ints>},
{"growth_while_wrapped", test_growth_while_wrapped<Ints>},
{"index_reads_and_writes", test_index_reads_and_writes<Ints>},
{"range_for", test_range_for<Ints>},
{"iterator_operators", test_iterator_operators<Ints>},
{"iterator_after_wrap", test_iterator_after_wrap<Ints>},
{"copy_is_independent", test_copy_is_independent<Ints>},
{"self_assignment", test_self_assignment<Ints>},
{"move_leaves_empty_and_usable", test_move_leaves_empty_and_usable<Ints>},
{"no_objects_left_behind", test_no_objects_left_behind<Deque<Tracked>>},
{"rvalue_push_does_not_copy", test_rvalue_push_does_not_copy<Deque<Tracked>>},
{"lvalue_push_copies_once", test_lvalue_push_copies_once<Deque<Tracked>>},
{"push_own_element", test_push_own_element<Deque<std::string>>},
{"pop_releases_value", test_pop_releases_value<Deque<std::shared_ptr<int>>>},
{"random_ints", [] {
for (unsigned seed : {1u, 2u, 3u}) {
random_against_std_deque<Deque<int>, int>(seed, 20000, [](unsigned x) {
return static_cast<int>(x % 1000);
});
}
}},
{"random_strings", [] {
random_against_std_deque<Deque<std::string>, std::string>(4u, 10000, [](unsigned x) {
return std::to_string(x % 1000) + std::string(x % 40, '-');
});
}},
};
}
std::vector<Test> tests_for(const std::string& kind) {
if (kind == "array") {
std::vector<Test> tests = common_tests<ArrayDeque>();
tests.push_back({"few_allocations", test_array_few_allocations<ArrayDeque<int>>});
tests.push_back({"capacity_doubles", test_array_capacity_doubles<ArrayDeque<int>>});
tests.push_back({"ring_reuses_space", test_array_ring_reuses_space<ArrayDeque<int>>});
tests.push_back({"move_is_noexcept", test_array_move_is_noexcept<ArrayDeque<int>>});
return tests;
}
std::vector<Test> tests = common_tests<LinkedDeque>();
tests.push_back({"one_allocation_per_push", test_linked_one_allocation_per_push<LinkedDeque<int>>});
tests.push_back({"elements_stay_in_place", test_linked_elements_stay_in_place<LinkedDeque<int>>});
tests.push_back({"move_assign_is_noexcept", test_linked_move_assign_is_noexcept<LinkedDeque<int>>});
tests.push_back({"shuffle_nodes", test_linked_shuffle_nodes<LinkedDeque<int>>});
return tests;
}
} // namespace
int main(int argc, char** argv) {
if (argc < 2 || (std::strcmp(argv[1], "array") != 0 && std::strcmp(argv[1], "linked") != 0)) {
std::cerr << "uso: deque_tests array|linked [prueba]\n";
return 2;
}
bool found = false;
for (const Test& test : tests_for(argv[1])) {
if (argc == 3 && std::strcmp(argv[2], "--list") == 0) {
std::cout << test.name << "\n";
found = true;
} else if (argc == 2 || std::strcmp(argv[2], test.name) == 0) {
test.run();
std::cout << argv[1] << " " << test.name << ": ok" << std::endl;
found = true;
}
}
if (!found) {
std::cerr << "no existe esa prueba\n";
return 2;
}
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 5: listas enlazadas y deques. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make lab compila el laboratorio en build/lab/
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-array prueba solo ArrayDeque (sesión 1 del laboratorio)
# make test-linked prueba solo LinkedDeque (sesión 2 del laboratorio)
# make test-lab prueba todo su laboratorio; falla hasta que lo terminen
# make bench mide sus dos deques, con -O2 y sin sanitizers
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
LAB_DIR ?= lab
BUILD = build
PROGRAMS = slist_demo dlist_demo trace_range invalidation erase_evens same_interface alloc_count test_structures
BENCHES = node_addresses traversal_bench ends_bench
COMMON_HEADERS = slist.h dlist.h tracked.h alloc_counter.h bench.h
LAB_HEADERS = $(LAB_DIR)/array_deque.h $(LAB_DIR)/linked_deque.h lab/not_implemented.h
.PHONY: all lab test test-array test-linked test-lab bench clean
all: $(PROGRAMS:%=$(BUILD)/%) $(BENCHES:%=$(BUILD)/%)
$(PROGRAMS:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(COMMON_HEADERS) | $(BUILD)
$(CXX) $(CXXFLAGS) $< -o $@
# Las mediciones van con -O2 y sin sanitizers.
$(BENCHES:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(COMMON_HEADERS) | $(BUILD)
$(CXX) $(BENCHFLAGS) $< -o $@
# LAB_DIR va primero: así las pruebas toman los deques de esa carpeta.
$(BUILD)/$(LAB_DIR)/deque_tests: lab/tests/deque_tests.cpp lab/tests/deque_suite.h $(LAB_HEADERS) $(COMMON_HEADERS) | $(BUILD)/$(LAB_DIR)
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) -Ilab -I. $< -o $@
$(BUILD)/$(LAB_DIR)/bench_deques: lab/tests/bench_deques.cpp $(LAB_HEADERS) $(COMMON_HEADERS) | $(BUILD)/$(LAB_DIR)
$(CXX) $(BENCHFLAGS) -I$(LAB_DIR) -Ilab -I. $< -o $@
$(BUILD) $(BUILD)/$(LAB_DIR):
mkdir -p $@
lab: $(BUILD)/$(LAB_DIR)/deque_tests $(BUILD)/$(LAB_DIR)/bench_deques
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
@if [ -d solution ]; then $(MAKE) --no-print-directory LAB_DIR=solution lab; fi
python3 -m unittest discover -p 'test_*.py'
test-array: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestArrayDeque
test-linked: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestLinkedDeque
test-lab: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab
bench: $(BUILD)/$(LAB_DIR)/bench_deques
$<
clean:
rm -rf $(BUILD) __pycache__