Semana 2 — Ownership y templates
Objetivos
- Escribir una clase que es owner de su memoria dinámica con RAII, sin fugas ni dobles liberaciones.
- Definir qué significa copiar y mover un objeto, y detectar una copia superficial.
- Escribir clases y funciones genéricas con templates.
- Distinguir un tipo abstracto de datos de su implementación, usando la pila como ejemplo.
Teoría — Ownership y RAII · Templates · Tipo abstracto e implementación, con la pila como ejemplo
Lectura: Apuntes, semana 2.
Apoyo: Open Data Structures, secs. 1.2 (Interfaces) y 2.1 (ArrayStack).
Temas:
- En C el owner era una convención. En C++,
new,deletey el destructor la vuelven parte del tipo: se libera al salir del alcance. - RAII: adquirir en el constructor y liberar en el destructor. El mismo owner de CS201, ahora automático.
- La copia superficial por defecto: dos objetos, un mismo arreglo, doble liberación. Se ve con ASan.
- Regla de cinco: destructor, constructor de copia, asignación de copia, constructor de movimiento y asignación de movimiento. Copiar con
copy and swap. - Mover: tomar los recursos de un objeto que ya no se va a usar.
std::moveno mueve nada por sí solo. - Templates:
template <typename T>. El compilador genera una versión por tipo, por eso el código va en el encabezado. Comparación convoid *y punteros a función de CS201. - Tipo abstracto de datos: qué operaciones ofrece y qué promete, sin decir cómo. Pila:
push,pop,top,empty. Implementación: un arreglo y un tope. - Invariantes: lo que siempre es cierto de una estructura entre dos operaciones. Una función
check_invariantspara las pruebas. - Error común: escribir el destructor y olvidar la copia. El programa compila y falla al final.
Laboratorio — Una pila genérica sin fugas
Sesión 1:
- Escribir
IntStack, sobre un arreglo de capacidad fija que se pide connew[]en el constructor, y su destructor. - Correr la prueba dada que copia una pila, leer el error de ASan y explicar con un dibujo por qué ocurre.
- Escribir el constructor de copia y la asignación de copia. Probar la autoasignación.
Sesión 2:
4. Agregar el constructor y la asignación de movimiento, y comprobar con un contador en la prueba que mover no reserva memoria.
5. Convertirla en el template Stack<T>, con check_invariants, y probarla con int y std::string.
6. Usarla en un programa real: un verificador de paréntesis, corchetes y llaves en archivos de código, que dice la línea del primer error. Se da hecha la parte que borra cadenas y comentarios; ustedes escriben la lógica con la pila.
Preguntas de repaso
- ¿Qué pasa si una clase con un
int *y un destructor que hacedelete[]se copia con la copia por defecto? Dibújenlo. - ¿Qué cinco funciones nombra la regla de cinco? ¿Por qué aparecen juntas?
- ¿En qué estado debe quedar un objeto después de que se mueve desde él?
- ¿Por qué el código de un template se escribe en el encabezado?
- ¿Qué es parte del tipo abstracto pila y qué es parte de su implementación:
push, el arreglo, el tope, “el último en entrar es el primero en salir”?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (22)Errores (17)Pruebas (7)Laboratorio (19)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Cuenta las reservas con new[] y las liberaciones con delete[] de todo el programa.
// Reemplaza esos operadores globales, así que se incluye en un solo .cpp de cada programa.
#ifndef ALLOC_COUNT_H
#define ALLOC_COUNT_H
#include <cstddef>
#include <new>
inline std::size_t& array_news() {
static std::size_t count = 0;
return count;
}
inline std::size_t& array_deletes() {
static std::size_t count = 0;
return count;
}
void* operator new[](std::size_t bytes) {
++array_news();
return ::operator new(bytes);
}
void operator delete[](void* block) noexcept {
if (block != nullptr) {
++array_deletes();
}
::operator delete(block);
}
void operator delete[](void* block, std::size_t) noexcept {
if (block != nullptr) {
++array_deletes();
}
::operator delete(block);
}
#endif// new T[n] construye los n elementos al reservar, aunque todavía nadie los use.
#include <iostream>
class Counted {
public:
Counted() {
constructed()++;
}
~Counted() {
destroyed()++;
}
static int& constructed() {
static int count = 0;
return count;
}
static int& destroyed() {
static int count = 0;
return count;
}
};
void report(const char* step) {
std::cout << step << ": construidos=" << Counted::constructed()
<< " destruidos=" << Counted::destroyed() << "\n";
}
int main() {
Counted* items = new Counted[1000];
report("después de new[]");
delete[] items;
report("después de delete[]");
return 0;
}// IntBuffer convertido en template: arreglo de tamaño fijo, de cualquier tipo T.
#ifndef BUFFER_H
#define BUFFER_H
#include <cassert>
#include <cstddef>
#include <utility>
// Exige que T tenga constructor sin argumentos, porque new T[size]() construye todos los
// elementos al reservar, y que se pueda asignar, porque la copia copia elemento por elemento.
//
// Invariante: data_ == nullptr si y solo si size_ == 0.
template <typename T>
class Buffer {
public:
explicit Buffer(std::size_t size) : data_(size == 0 ? nullptr : new T[size]()), size_(size) {}
~Buffer() {
delete[] data_;
}
Buffer(const Buffer& other) : Buffer(other.size_) {
for (std::size_t i = 0; i < size_; i++) {
data_[i] = other.data_[i];
}
}
Buffer& operator=(const Buffer& other) {
Buffer copy(other);
swap(copy);
return *this;
}
Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
Buffer& operator=(Buffer&& other) noexcept {
Buffer taken(std::move(other));
swap(taken);
return *this;
}
std::size_t size() const {
return size_;
}
T& at(std::size_t index) {
assert(index < size_);
return data_[index];
}
const T& at(std::size_t index) const {
assert(index < size_);
return data_[index];
}
void swap(Buffer& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}
void check_invariants() const {
assert((data_ == nullptr) == (size_ == 0));
}
private:
T* data_;
std::size_t size_;
};
#endif// Ayuda mínima para las pruebas: anuncia cada prueba y confirma que todas terminaron.
#ifndef CHECK_H
#define CHECK_H
#include <iostream>
template <typename Test>
void run_test(const char* name, Test test) {
test();
std::cout << "ok " << name << "\n";
}
#endif// Copia profunda y copy and swap, con las reservas contadas.
#include <iostream>
#include "alloc_count.h"
#include "int_buffer.h"
void report(const char* step, const IntBuffer& a, const IntBuffer& b) {
std::cout << step << ": a.at(0)=" << a.at(0) << " b.at(0)=" << b.at(0)
<< " | new[]=" << array_news() << " delete[]=" << array_deletes() << "\n";
}
int main() {
IntBuffer a(3);
a.at(0) = 10;
IntBuffer b(a);
report("copia", a, b);
b.at(0) = 99;
report("cambio b", a, b);
IntBuffer c(5);
c.at(0) = 7;
b = c;
report("b = c", a, b);
std::cout << "b.size()=" << b.size() << "\n";
IntBuffer& same = b;
b = same;
report("b = b", a, b);
std::cout << "b.size()=" << b.size() << "\n";
return 0;
}// Declarar un destructor apaga el movimiento implícito: "mover" termina copiando.
#include <iostream>
#include <utility>
#include "traced.h"
class OnlyMember {
public:
Traced member{"m"};
};
class WithDestructor {
public:
~WithDestructor() {}
Traced member{"m"};
};
int main() {
std::cout << "sin destructor declarado:\n";
OnlyMember a;
OnlyMember b = std::move(a);
std::cout << "con destructor declarado:\n";
WithDestructor c;
WithDestructor d = std::move(c);
std::cout << "fin de main\n";
return 0;
}#include "int_buffer.h"
#include <cassert>
#include <utility>
IntBuffer::IntBuffer(std::size_t size)
: data_(size == 0 ? nullptr : new int[size]()), size_(size) {}
IntBuffer::~IntBuffer() {
delete[] data_;
}
IntBuffer::IntBuffer(const IntBuffer& other) : IntBuffer(other.size_) {
for (std::size_t i = 0; i < size_; i++) {
data_[i] = other.data_[i];
}
}
IntBuffer& IntBuffer::operator=(const IntBuffer& other) {
IntBuffer copy(other);
swap(copy);
return *this;
}
IntBuffer::IntBuffer(IntBuffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
IntBuffer& IntBuffer::operator=(IntBuffer&& other) noexcept {
IntBuffer taken(std::move(other));
swap(taken);
return *this;
}
std::size_t IntBuffer::size() const {
return size_;
}
int& IntBuffer::at(std::size_t index) {
assert(index < size_);
return data_[index];
}
int IntBuffer::at(std::size_t index) const {
assert(index < size_);
return data_[index];
}
void IntBuffer::swap(IntBuffer& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}
void IntBuffer::check_invariants() const {
assert((data_ == nullptr) == (size_ == 0));
}// Arreglo de enteros de tamaño fijo, owner de su memoria.
#ifndef INT_BUFFER_H
#define INT_BUFFER_H
#include <cstddef>
// Invariantes:
// 1. data_ == nullptr si y solo si size_ == 0.
// 2. Si size_ > 0, data_ apunta a un bloque de size_ enteros, y este objeto es su único owner.
class IntBuffer {
public:
explicit IntBuffer(std::size_t size);
~IntBuffer();
IntBuffer(const IntBuffer& other);
IntBuffer& operator=(const IntBuffer& other);
IntBuffer(IntBuffer&& other) noexcept;
IntBuffer& operator=(IntBuffer&& other) noexcept;
std::size_t size() const;
int& at(std::size_t index);
int at(std::size_t index) const;
void swap(IntBuffer& other) noexcept;
void check_invariants() const;
private:
int* data_;
std::size_t size_;
};
#endif// Tras mover, un string o un vector es válido, pero su valor no está especificado.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::string text = "un texto bastante largo para no caber en el objeto";
std::string other = std::move(text);
std::cout << "text.size()=" << text.size() << " other.size()=" << other.size() << "\n";
text = "de nuevo";
std::cout << text << "\n";
std::vector<int> numbers{1, 2, 3};
std::vector<int> taken = std::move(numbers);
numbers.clear();
numbers.push_back(9);
std::cout << "numbers.size()=" << numbers.size() << " taken.size()=" << taken.size() << "\n";
return 0;
}// Máximo genérico con un template: el tipo se comprueba y se genera una versión por tipo.
#include <cassert>
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>
template <typename T>
const T& max_of(const std::vector<T>& items) {
assert(!items.empty());
const T* best = &items[0];
for (std::size_t i = 1; i < items.size(); i++) {
if (items[i] > *best) {
best = &items[i];
}
}
return *best;
}
int main() {
std::vector<int> numbers{3, 9, 4};
std::vector<std::string> names{"Ana", "Zoila", "Beto"};
std::cout << max_of(numbers) << "\n";
std::cout << max_of(names) << "\n";
return 0;
}// Máximo genérico al estilo de CS201: void *, tamaños y un puntero a función.
#include <stddef.h>
#include <stdio.h>
typedef int (*compare_fn)(const void *, const void *);
const void *max_of(const void *items, size_t count, size_t size, compare_fn compare) {
const char *bytes = items;
const void *best = items;
for (size_t i = 1; i < count; i++) {
const void *item = bytes + i * size;
if (compare(item, best) > 0) {
best = item;
}
}
return best;
}
int compare_int(const void *a, const void *b) {
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}
int compare_double(const void *a, const void *b) {
double x = *(const double *)a;
double y = *(const double *)b;
return (x > y) - (x < y);
}
int main(void) {
int numbers[] = {3, 9, 4};
const int *best = max_of(numbers, 3, sizeof numbers[0], compare_int);
printf("%d\n", *best);
double prices[] = {1.5, 0.25, 7.75};
const double *top = max_of(prices, 3, sizeof prices[0], compare_double);
printf("%.2f\n", *top);
return 0;
}// std::move solo es un cast: quien mueve es el constructor o la asignación que se elige.
#include <iostream>
#include <utility>
#include "alloc_count.h"
#include "int_buffer.h"
void report(const char* step) {
std::cout << step << ": new[]=" << array_news() << " delete[]=" << array_deletes() << "\n";
}
int main() {
IntBuffer a(1000);
a.at(0) = 42;
report("a creado");
IntBuffer&& same = std::move(a);
report("solo std::move(a)");
std::cout << "same.size()=" << same.size() << " a.size()=" << a.size() << " a.at(0)=" << a.at(0) << "\n";
IntBuffer b = std::move(a);
report("constructor de movimiento");
std::cout << "a.size()=" << a.size() << " b.size()=" << b.size() << " b.at(0)=" << b.at(0) << "\n";
a.check_invariants();
IntBuffer c(10);
report("c creado");
c = std::move(b);
report("asignación de movimiento");
std::cout << "b.size()=" << b.size() << " c.size()=" << c.size() << " c.at(0)=" << c.at(0) << "\n";
b.check_invariants();
b = c;
report("se reutiliza b, con una copia");
std::cout << "b.size()=" << b.size() << " b.at(0)=" << b.at(0) << "\n";
const IntBuffer d(20);
report("d creado, const");
IntBuffer e = std::move(d);
report("std::move de un const");
std::cout << "d.size()=" << d.size() << " e.size()=" << e.size() << "\n";
return 0;
}// new y delete: un objeto, y un arreglo. Cada new con su delete del mismo tipo.
#include <cstddef>
#include <iostream>
#include <string>
int main() {
int* one = new int(7);
std::string* name = new std::string("pila");
std::cout << *one << " " << *name << " " << name->size() << "\n";
delete one;
delete name;
std::size_t n = 4;
int* zeros = new int[n]();
std::string* words = new std::string[n];
words[1] = "hola";
std::cout << zeros[3] << " [" << words[0] << "] [" << words[1] << "]\n";
delete[] zeros;
delete[] words;
int* nothing = nullptr;
delete nothing;
std::cout << "delete de nullptr: no hace nada\n";
return 0;
}// std::vector no necesita un constructor sin argumentos para push_back: separa reservar de construir.
#include <iostream>
#include <string>
#include <vector>
class Student {
public:
explicit Student(const std::string& name) : name_(name) {}
const std::string& name() const {
return name_;
}
private:
std::string name_;
};
int main() {
std::vector<Student> students;
students.push_back(Student("Ana"));
std::cout << students[0].name() << "\n";
return 0;
}// El owner como convención: nada en el tipo obliga a llamar a int_array_free.
#include <stdio.h>
#include <stdlib.h>
struct int_array {
int *data;
size_t size;
};
struct int_array int_array_new(size_t size) {
struct int_array array;
array.data = calloc(size, sizeof *array.data);
array.size = array.data == NULL ? 0 : size;
return array;
}
void int_array_free(struct int_array *array) {
free(array->data);
array->data = NULL;
array->size = 0;
}
int main(void) {
struct int_array numbers = int_array_new(4);
if (numbers.data == NULL) {
return 1;
}
numbers.data[0] = 7;
printf("%d\n", numbers.data[0]);
int_array_free(&numbers);
return 0;
}// Calculadora en notación postfija: un uso de la pila que no sabe cómo está hecha.
#include <cassert>
#include <iostream>
#include <string>
#include <vector>
#include "vector_stack.h"
int eval_postfix(const std::vector<std::string>& tokens) {
VectorStack<int> values;
for (const std::string& token : tokens) {
if (token == "+" || token == "-" || token == "*") {
assert(values.size() >= 2);
int right = values.top();
values.pop();
int left = values.top();
values.pop();
if (token == "+") {
values.push(left + right);
} else if (token == "-") {
values.push(left - right);
} else {
values.push(left * right);
}
} else {
values.push(std::stoi(token));
}
}
assert(values.size() == 1);
return values.top();
}
int main() {
std::cout << eval_postfix({"3", "4", "+", "2", "*"}) << "\n";
std::cout << eval_postfix({"10", "2", "3", "*", "-"}) << "\n";
return 0;
}// Muestra cuándo corre cada destructor.
#include <iostream>
#include <string>
class Noisy {
public:
explicit Noisy(const std::string& name) : name_(name) {
std::cout << " crea " << name_ << "\n";
}
~Noisy() {
std::cout << " destruye " << name_ << "\n";
}
private:
std::string name_;
};
int first_positive(int a, int b) {
Noisy guard("guard");
if (a > 0) {
return a;
}
return b;
}
int main() {
std::cout << "bloque:\n";
{
Noisy x("x");
Noisy y("y");
std::cout << " fin del bloque\n";
}
std::cout << "retorno temprano:\n";
int result = first_positive(5, 6);
std::cout << " resultado " << result << "\n";
std::cout << "new y delete:\n";
Noisy* z = new Noisy("z");
std::cout << " z sigue vivo\n";
delete z;
std::cout << "fin de main\n";
return 0;
}// Enteros siempre ordenados. Un ejemplo de invariante que sí cuesta comprobar.
#ifndef SORTED_INTS_H
#define SORTED_INTS_H
#include <cassert>
#include <cstddef>
#include <vector>
// Invariante: items_[i - 1] <= items_[i] para todo i válido.
class SortedInts {
public:
void insert(int value) {
items_.push_back(value);
std::size_t i = items_.size() - 1;
while (i > 0 && items_[i - 1] > value) {
items_[i] = items_[i - 1];
i--;
}
items_[i] = value;
}
int smallest() const {
assert(!items_.empty());
return items_[0];
}
std::size_t size() const {
return items_.size();
}
void check_invariants() const {
for (std::size_t i = 1; i < items_.size(); i++) {
assert(items_[i - 1] <= items_[i]);
}
}
private:
std::vector<int> items_;
};
#endif// Qué función especial elige el compilador en cada línea.
#include <iostream>
#include <utility>
#include "traced.h"
Traced make_traced() {
Traced local("local");
return local;
}
int main() {
std::cout << "Traced a(\"a\");\n";
Traced a("a");
std::cout << "Traced b = a;\n";
Traced b = a;
std::cout << "Traced c = std::move(a);\n";
Traced c = std::move(a);
std::cout << "b = c;\n";
b = c;
std::cout << "b = std::move(c);\n";
b = std::move(c);
std::cout << "Traced d = make_traced();\n";
Traced d = make_traced();
std::cout << "fin de main\n";
return 0;
}// Clase que escribe qué función especial corre en cada momento.
#ifndef TRACED_H
#define TRACED_H
#include <iostream>
#include <string>
#include <utility>
class Traced {
public:
explicit Traced(const std::string& name) : name_(name) {
std::cout << " constructor " << name_ << "\n";
}
~Traced() {
std::cout << " destructor " << name_ << "\n";
}
Traced(const Traced& other) : name_(other.name_ + "+c") {
std::cout << " constructor de copia, desde " << other.name_ << "\n";
}
Traced& operator=(const Traced& other) {
std::cout << " asignación de copia, " << name_ << " = " << other.name_ << "\n";
return *this;
}
Traced(Traced&& other) noexcept : name_(other.name_ + "+m") {
std::cout << " constructor de movimiento, desde " << other.name_ << "\n";
}
Traced& operator=(Traced&& other) noexcept {
std::cout << " asignación de movimiento, " << name_ << " = " << other.name_ << "\n";
return *this;
}
private:
std::string name_;
};
#endif// Al crecer, std::vector mueve sus elementos solo si mover no puede lanzar una excepción.
#include <iostream>
#include <type_traits>
#include <utility>
#include <vector>
struct Counts {
int copies = 0;
int moves = 0;
};
class SafeMove {
public:
explicit SafeMove(Counts& counts) : counts_(&counts) {}
SafeMove(const SafeMove& other) : counts_(other.counts_) {
counts_->copies++;
}
SafeMove(SafeMove&& other) noexcept : counts_(other.counts_) {
counts_->moves++;
}
private:
Counts* counts_;
};
class RiskyMove {
public:
explicit RiskyMove(Counts& counts) : counts_(&counts) {}
RiskyMove(const RiskyMove& other) : counts_(other.counts_) {
counts_->copies++;
}
RiskyMove(RiskyMove&& other) : counts_(other.counts_) {
counts_->moves++;
}
private:
Counts* counts_;
};
template <typename T>
void grow(const char* name) {
Counts counts;
std::vector<T> items;
items.reserve(1000);
for (int i = 0; i < 1000; i++) {
items.emplace_back(counts);
}
std::cout << name << " nothrow=" << std::is_nothrow_move_constructible_v<T>
<< " antes de crecer: copias=" << counts.copies << " movimientos=" << counts.moves << "\n";
items.reserve(2000);
std::cout << name << " después de crecer: copias=" << counts.copies
<< " movimientos=" << counts.moves << "\n";
}
int main() {
grow<SafeMove>("SafeMove");
grow<RiskyMove>("RiskyMove");
return 0;
}// La misma pila, sobre std::vector. Otra implementación del mismo tipo abstracto.
#ifndef VECTOR_STACK_H
#define VECTOR_STACK_H
#include <cassert>
#include <cstddef>
#include <utility>
#include <vector>
// No declara ninguna de las cinco funciones: std::vector ya se encarga (regla de cero).
template <typename T>
class VectorStack {
public:
void push(T value) {
items_.push_back(std::move(value));
}
void pop() {
assert(!empty());
items_.pop_back();
}
T& top() {
assert(!empty());
return items_.back();
}
bool empty() const {
return items_.empty();
}
std::size_t size() const {
return items_.size();
}
// Sin invariantes propios: cualquier estado de items_ es una pila válida.
void check_invariants() const {}
private:
std::vector<T> items_;
};
#endifErrores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// No compila: una asignación por valor choca con una asignación de movimiento.
#include <utility>
class Both {
public:
Both() = default;
Both(const Both&) = default;
Both(Both&&) noexcept = default;
Both& operator=(Both) {
return *this;
}
Both& operator=(Both&&) noexcept {
return *this;
}
};
int main() {
Both a;
Both b;
b = std::move(a);
return 0;
}// Error: insert deja de correr elementos un paso antes. El invariante lo descubre.
#include <cassert>
#include <cstddef>
#include <iostream>
#include <vector>
class SortedInts {
public:
void insert(int value) {
items_.push_back(value);
std::size_t i = items_.size() - 1;
while (i > 1 && items_[i - 1] > value) {
items_[i] = items_[i - 1];
i--;
}
items_[i] = value;
}
std::size_t size() const {
return items_.size();
}
void check_invariants() const {
for (std::size_t i = 1; i < items_.size(); i++) {
assert(items_[i - 1] <= items_[i]);
}
}
private:
std::vector<int> items_;
};
int main() {
SortedInts sorted;
sorted.insert(5);
sorted.check_invariants();
sorted.insert(3);
sorted.check_invariants();
std::cout << "tamaño " << sorted.size() << "\n";
return 0;
}// No compila: declarar un constructor de movimiento borra la copia implícita.
#include <utility>
class OnlyMoves {
public:
OnlyMoves() = default;
OnlyMoves(OnlyMoves&&) noexcept {}
};
int main() {
OnlyMoves a;
OnlyMoves b = a;
return 0;
}// Error: un bloque de new[] se libera con delete, sin corchetes.
int main() {
int* numbers = new int[4]();
numbers[0] = 1;
delete numbers;
return 0;
}// No compila: new T[size] necesita construir sin argumentos, y Student no lo permite.
#include <string>
#include "../buffer.h"
class Student {
public:
explicit Student(const std::string& name) : name_(name) {}
private:
std::string name_;
};
int main() {
Buffer<Student> students(3);
return 0;
}// No compila: max_of usa >, y Point no lo tiene. El error aparece al usar el template con Point.
#include <cassert>
#include <cstddef>
#include <vector>
template <typename T>
const T& max_of(const std::vector<T>& items) {
assert(!items.empty());
const T* best = &items[0];
for (std::size_t i = 1; i < items.size(); i++) {
if (items[i] > *best) {
best = &items[i];
}
}
return *best;
}
struct Point {
int x;
int y;
};
int main() {
std::vector<Point> points{{1, 2}, {3, 4}};
max_of(points);
return 0;
}// Aviso: std::move en el return impide la elisión de la copia. Compila, pero es peor.
#include <utility>
#include "../traced.h"
Traced make_traced() {
Traced local("local");
return std::move(local);
}
int main() {
Traced d = make_traced();
return 0;
}// Error: la asignación libera primero. Con a = a, destruye los datos que iba a copiar.
#include <cstddef>
#include <iostream>
class IntBuffer {
public:
explicit IntBuffer(std::size_t size) : data_(new int[size]()), size_(size) {}
~IntBuffer() {
delete[] data_;
}
int& at(std::size_t index) {
return data_[index];
}
IntBuffer(const IntBuffer& other) : data_(new int[other.size_]), size_(other.size_) {
for (std::size_t i = 0; i < size_; i++) {
data_[i] = other.data_[i];
}
}
IntBuffer& operator=(const IntBuffer& other) {
delete[] data_;
data_ = new int[other.size_];
size_ = other.size_;
for (std::size_t i = 0; i < size_; i++) {
data_[i] = other.data_[i];
}
return *this;
}
private:
int* data_;
std::size_t size_;
};
int main() {
IntBuffer a(3);
a.at(0) = 10;
std::cout << "antes: " << a.at(0) << "\n";
IntBuffer& same = a;
a = same;
std::cout << "después: " << a.at(0) << "\n";
return 0;
}// Error: la asignación por defecto pierde el bloque del destino y comparte el del origen.
#include <cstddef>
class IntBuffer {
public:
explicit IntBuffer(std::size_t size) : data_(new int[size]()), size_(size) {}
~IntBuffer() {
delete[] data_;
}
private:
int* data_;
std::size_t size_;
};
int main() {
IntBuffer a(3);
IntBuffer b(5);
b = a;
return 0;
}// Error: la clase tiene destructor, pero la copia por defecto copia el puntero.
#include <cstddef>
#include <iostream>
class IntBuffer {
public:
explicit IntBuffer(std::size_t size) : data_(new int[size]()), size_(size) {}
~IntBuffer() {
delete[] data_;
}
int& at(std::size_t index) {
return data_[index];
}
std::size_t size() const {
return size_;
}
private:
int* data_;
std::size_t size_;
};
int main() {
IntBuffer a(3);
a.at(0) = 10;
IntBuffer b = a;
b.at(0) = 99;
std::cout << "a.at(0) = " << a.at(0) << "\n";
return 0;
}// Error: con un destructor declarado no hay movimiento implícito; std::move termina en la copia superficial.
#include <cstddef>
#include <utility>
class IntBuffer {
public:
explicit IntBuffer(std::size_t size) : data_(new int[size]()), size_(size) {}
~IntBuffer() {
delete[] data_;
}
private:
int* data_;
std::size_t size_;
};
int main() {
IntBuffer a(3);
IntBuffer b = std::move(a);
return 0;
}// No compila: el tipo que devuelve el template no se convierte en otro tipo sin relación.
#include <cassert>
#include <cstddef>
#include <vector>
template <typename T>
const T& max_of(const std::vector<T>& items) {
assert(!items.empty());
const T* best = &items[0];
for (std::size_t i = 1; i < items.size(); i++) {
if (items[i] > *best) {
best = &items[i];
}
}
return *best;
}
int main() {
std::vector<double> prices{1.5, 0.25, 7.75};
const int* top = &max_of(prices);
return top == nullptr;
}// Error: se lee un elemento de un objeto del que ya se movió.
#include <iostream>
#include <utility>
#include "../int_buffer.h"
int main() {
IntBuffer a(3);
IntBuffer b = std::move(a);
std::cout << "b.size() = " << b.size() << "\n";
std::cout << a.at(0) << "\n";
return 0;
}// Error: un arreglo de double con la comparación de int. Compila sin avisos y responde mal.
#include <stddef.h>
#include <stdio.h>
typedef int (*compare_fn)(const void *, const void *);
const void *max_of(const void *items, size_t count, size_t size, compare_fn compare) {
const char *bytes = items;
const void *best = items;
for (size_t i = 1; i < count; i++) {
const void *item = bytes + i * size;
if (compare(item, best) > 0) {
best = item;
}
}
return best;
}
int compare_int(const void *a, const void *b) {
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}
int main(void) {
double prices[] = {1.5, 0.25, 7.75};
const double *top = max_of(prices, 3, sizeof prices[0], compare_int);
printf("%.2f\n", *top);
return 0;
}// Error: al compilar este archivo nadie pide Box<int>, así que no se genera nada.
#include "box.h"
template <typename T>
Box<T>::Box(T value) : value_(value) {}
template <typename T>
const T& Box<T>::value() const {
return value_;
}// El template se declara aquí, pero se define en box.cpp.
#ifndef BOX_H
#define BOX_H
template <typename T>
class Box {
public:
explicit Box(T value);
const T& value() const;
private:
T value_;
};
#endif// Pide Box<int>, pero desde aquí no se ve el cuerpo de sus métodos.
#include <iostream>
#include "box.h"
int main() {
Box<int> box(5);
std::cout << box.value() << "\n";
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
// Pruebas de Buffer<T>, con int, std::string y un tipo cuya copia puede fallar.
#include <cassert>
#include <stdexcept>
#include <string>
#include <utility>
#include "buffer.h"
#include "check.h"
// Lanza una excepción en la copia número fail_at. El contador va en la clase, sin variables globales.
class Fragile {
public:
Fragile() = default;
Fragile& operator=(const Fragile& other) {
value_ = other.value_;
if (++assignments() == fail_at()) {
throw std::runtime_error("copia fallida");
}
return *this;
}
static int& assignments() {
static int count = 0;
return count;
}
static int& fail_at() {
static int position = 0;
return position;
}
int value_ = 0;
};
int main() {
run_test("int", [] {
Buffer<int> a(3);
assert(a.at(2) == 0);
a.at(2) = 7;
Buffer<int> b(a);
b.at(2) = 8;
assert(a.at(2) == 7 && b.at(2) == 8);
});
run_test("string", [] {
Buffer<std::string> a(2);
a.at(0) = "uno";
Buffer<std::string> b(1);
b = a;
a.at(0) = "dos";
assert(b.at(0) == "uno" && b.size() == 2);
Buffer<std::string> c(std::move(a));
assert(c.at(0) == "dos" && a.size() == 0);
a.check_invariants();
Buffer<std::string>& same = c;
c = same;
c = std::move(same);
assert(c.at(0) == "dos");
});
run_test("si la copia falla, el constructor no deja fuga", [] {
Buffer<Fragile> source(4);
Fragile::assignments() = 0;
Fragile::fail_at() = 2;
bool thrown = false;
try {
Buffer<Fragile> copy(source);
} catch (const std::runtime_error&) {
thrown = true;
}
assert(thrown);
});
run_test("si la copia falla, el destino de la asignación no cambia", [] {
Buffer<Fragile> source(4);
Buffer<Fragile> target(2);
target.at(0).value_ = 123;
Fragile::assignments() = 0;
Fragile::fail_at() = 2;
bool thrown = false;
try {
target = source;
} catch (const std::runtime_error&) {
thrown = true;
}
assert(thrown);
assert(target.size() == 2 && target.at(0).value_ == 123);
Fragile::fail_at() = 0;
});
return 0;
}// Pruebas de IntBuffer: copia, autoasignación, movimiento y cuántas reservas hace cada uno.
#include <cassert>
#include <type_traits>
#include <utility>
#include "alloc_count.h"
#include "check.h"
#include "int_buffer.h"
static_assert(std::is_nothrow_move_constructible_v<IntBuffer>);
static_assert(std::is_nothrow_move_assignable_v<IntBuffer>);
int main() {
run_test("copia independiente", [] {
IntBuffer a(3);
a.at(1) = 5;
IntBuffer b(a);
b.at(1) = 6;
assert(a.at(1) == 5 && b.at(1) == 6);
});
run_test("asignación de copia reserva una vez", [] {
IntBuffer a(3);
IntBuffer b(8);
a.at(0) = 1;
std::size_t before = array_news();
b = a;
assert(array_news() == before + 1);
assert(b.size() == 3 && b.at(0) == 1);
a.at(0) = 2;
assert(b.at(0) == 1);
});
run_test("autoasignación", [] {
IntBuffer a(3);
a.at(2) = 4;
IntBuffer& same = a;
a = same;
assert(a.size() == 3 && a.at(2) == 4);
});
run_test("mover no reserva y deja vacío", [] {
IntBuffer a(3);
a.at(0) = 8;
std::size_t before = array_news();
IntBuffer b(std::move(a));
assert(array_news() == before);
assert(a.size() == 0 && b.at(0) == 8);
a.check_invariants();
IntBuffer c(1);
before = array_news();
c = std::move(b);
assert(array_news() == before);
assert(b.size() == 0 && c.size() == 3 && c.at(0) == 8);
b.check_invariants();
});
run_test("automovimiento", [] {
IntBuffer a(2);
a.at(1) = 3;
IntBuffer& same = a;
a = std::move(same);
assert(a.size() == 2 && a.at(1) == 3);
a.check_invariants();
});
run_test("un objeto movido se puede volver a usar", [] {
IntBuffer a(2);
IntBuffer b(std::move(a));
a = b;
assert(a.size() == 2);
IntBuffer c(std::move(a));
a = IntBuffer(4);
assert(a.size() == 4);
});
run_test("reservas y liberaciones se emparejan", [] {
std::size_t news = array_news();
std::size_t deletes = array_deletes();
{
IntBuffer a(3);
IntBuffer b(a);
IntBuffer c(std::move(a));
b = c;
c = std::move(b);
}
assert(array_news() - news == array_deletes() - deletes);
});
return 0;
}// Pruebas de SortedInts: al azar con semilla fija, contra std::sort, con invariantes en cada paso.
#include <algorithm>
#include <cassert>
#include <random>
#include <vector>
#include "check.h"
#include "sorted_ints.h"
int main() {
run_test("inserciones al azar", [] {
std::mt19937 random(2026);
std::uniform_int_distribution<int> value(-50, 50);
SortedInts sorted;
std::vector<int> reference;
for (int i = 0; i < 2000; i++) {
int x = value(random);
sorted.insert(x);
reference.push_back(x);
sorted.check_invariants();
std::sort(reference.begin(), reference.end());
assert(sorted.size() == reference.size());
assert(sorted.smallest() == reference[0]);
}
});
return 0;
}"""Corre los programas de la semana y comprueba lo que los apuntes dicen de ellos.
Los programas de errors/ se compilan aquí: deben fallar de la manera anunciada."""
import os
import pathlib
import shutil
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
CXXFLAGS = ["-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]
BUILD = HERE / "build"
SOLUTION = HERE / "solution"
LAB_TESTS = {
"Step1Basics": "paso 1",
"Step3Copy": "paso 3",
"Step4Move": "paso 4",
"Step5Template": "paso 5",
"Step6Brackets": "paso 6",
}
def run(args, env=None):
return subprocess.run(
args, cwd=HERE, text=True, encoding="utf-8", errors="replace", env=env,
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=300,
)
def compile_error(source, *others):
"""Compila un programa de errors/ y devuelve (código, salida, ruta del ejecutable)."""
target = BUILD / ("check_" + pathlib.Path(source).stem)
done = run(["g++", *CXXFLAGS, source, *others, "-o", str(target)])
return done.returncode, done.stdout, target
class Programs(unittest.TestCase):
def output(self, program):
done = run([str(BUILD / program)])
self.assertEqual(done.returncode, 0, done.stdout)
return done.stdout
def test_destructors_run_in_reverse_order(self):
text = self.output("scope_trace")
self.assertLess(text.index("destruye y"), text.index("destruye x"))
self.assertLess(text.index("destruye guard"), text.index("resultado 5"))
def test_copy_is_deep_and_counted(self):
text = self.output("copy_demo")
self.assertIn("copia: a.at(0)=10 b.at(0)=10 | new[]=2 delete[]=0", text)
self.assertIn("cambio b: a.at(0)=10 b.at(0)=99 | new[]=2 delete[]=0", text)
self.assertIn("b = c: a.at(0)=10 b.at(0)=7 | new[]=4 delete[]=1", text)
self.assertIn("b = b: a.at(0)=10 b.at(0)=7 | new[]=5 delete[]=2", text)
def test_move_does_not_reserve(self):
text = self.output("move_demo")
self.assertIn("solo std::move(a): new[]=1 delete[]=0", text)
self.assertIn("same.size()=1000 a.size()=1000 a.at(0)=42", text)
self.assertIn("constructor de movimiento: new[]=1 delete[]=0", text)
self.assertIn("a.size()=0 b.size()=1000 b.at(0)=42", text)
self.assertIn("asignación de movimiento: new[]=2 delete[]=1", text)
self.assertIn("std::move de un const: new[]=5 delete[]=1", text)
def test_returning_a_local_is_elided(self):
text = self.output("special_members")
tail = text[text.index("Traced d = make_traced();"):text.index("fin de main")]
self.assertNotIn("movimiento", tail)
self.assertNotIn("copia", tail)
def test_destructor_turns_off_implicit_move(self):
text = self.output("implicit_moves")
first = text[:text.index("con destructor declarado")]
second = text[text.index("con destructor declarado"):]
self.assertIn("constructor de movimiento", first)
self.assertIn("constructor de copia", second)
def test_vector_growth_depends_on_noexcept(self):
text = self.output("vector_growth")
self.assertIn("SafeMove después de crecer: copias=0 movimientos=1000", text)
self.assertIn("RiskyMove después de crecer: copias=1000 movimientos=0", text)
def test_unit_tests(self):
for program in ["int_buffer_test", "buffer_test", "sorted_ints_test", "vector_stack_test"]:
with self.subTest(program=program):
self.assertNotIn("Assertion", self.output(program))
def test_postfix(self):
self.assertEqual(self.output("postfix").split(), ["14", "4"])
class ErrorPrograms(unittest.TestCase):
def test_silent_compile_then_double_free(self):
for source in ["shallow_copy", "shallow_assign", "shallow_move"]:
with self.subTest(source=source):
code, text, target = compile_error(f"errors/{source}.cpp")
self.assertEqual((code, text), (0, ""), "debe compilar sin avisos")
done = run([str(target)])
self.assertNotEqual(done.returncode, 0)
self.assertIn("attempting double-free", done.stdout)
def test_new_delete_mismatch(self):
code, text, target = compile_error("errors/new_delete_mismatch.cpp")
self.assertEqual(code, 0)
self.assertIn("-Wmismatched-new-delete", text)
self.assertIn("alloc-dealloc-mismatch", run([str(target)]).stdout)
def test_naive_self_assignment_loses_the_data_silently(self):
code, text, target = compile_error("errors/self_assign.cpp")
self.assertEqual((code, text), (0, ""))
done = run([str(target)])
self.assertEqual(done.returncode, 0, "ASan no lo reporta")
lines = done.stdout.splitlines()
self.assertEqual(lines[0], "antes: 10")
self.assertNotEqual(lines[1], "después: 10")
def test_use_after_move_is_caught_by_assert(self):
code, text, target = compile_error("errors/use_after_move.cpp", "int_buffer.cpp")
self.assertEqual((code, text), (0, ""))
done = run([str(target)])
self.assertNotEqual(done.returncode, 0)
self.assertIn("Assertion `index < size_' failed", done.stdout)
def test_broken_invariant_is_caught(self):
code, _, target = compile_error("errors/broken_sorted.cpp")
self.assertEqual(code, 0)
self.assertIn("Assertion `items_[i - 1] <= items_[i]' failed", run([str(target)]).stdout)
def test_programs_that_do_not_compile(self):
expected = {
"move_deletes_copy": "use of deleted function",
"ambiguous_assign": "ambiguous overload",
"no_default": "no matching function for call to",
"no_operator": "no match for",
"template_wrong_type": "cannot convert",
}
for source, message in expected.items():
with self.subTest(source=source):
code, text, _ = compile_error(f"errors/{source}.cpp")
self.assertNotEqual(code, 0)
self.assertIn(message, text)
def test_template_in_cpp_does_not_link(self):
code, text, _ = compile_error("errors/split_template/main.cpp", "errors/split_template/box.cpp")
self.assertNotEqual(code, 0)
self.assertIn("undefined reference to `Box<int>::Box(int)'", text)
def test_void_pointer_accepts_the_wrong_comparison(self):
target = BUILD / "check_void_wrong_compare"
done = run(["gcc", "-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined",
"errors/void_wrong_compare.c", "-o", str(target)])
self.assertEqual((done.returncode, done.stdout), (0, ""))
self.assertEqual(run([str(target)]).stdout.strip(), "1.50")
class NoWarnings(unittest.TestCase):
def test_everything_compiles_cleanly(self):
sources = sorted(HERE.glob("*.cpp")) + sorted((HERE / "lab").glob("*.cpp"))
sources += sorted((HERE / "lab" / "tests").glob("*.cpp"))
if SOLUTION.exists():
sources += sorted(SOLUTION.glob("*.cpp"))
for source in sources:
with self.subTest(source=source.name):
extra = ["-Isolution"] if source.parent == SOLUTION else ["-Ilab"]
done = run(["g++", "-Wall", "-Wextra", "-std=c++20", "-fsyntax-only", *extra, "-Ilab",
str(source.relative_to(HERE))])
self.assertEqual((done.returncode, done.stdout), (0, ""))
@unittest.skipUnless(SOLUTION.exists(), "falta la solución")
class LabStages(unittest.TestCase):
"""El laboratorio funciona como dice la guía, paso a paso. Se arma cada etapa quitando
partes de la solución, en una carpeta temporal dentro de build/."""
@staticmethod
def stage(name, keep_copy, keep_move):
base = pathlib.Path(tempfile.mkdtemp(prefix=name, dir=BUILD))
shutil.copytree(SOLUTION, base, dirs_exist_ok=True)
header = (base / "int_stack.h").read_text(encoding="utf-8")
source = (base / "int_stack.cpp").read_text(encoding="utf-8")
if not keep_copy:
header = header.replace(" IntStack(const IntStack& other);\n", "")
header = header.replace(" IntStack& operator=(const IntStack& other);\n", "")
source = source.split("IntStack::IntStack(const IntStack& other)")[0] + \
"void IntStack::swap" + source.split("void IntStack::swap")[1]
if not keep_move:
header = header.replace(" IntStack(IntStack&& other) noexcept;\n", "")
header = header.replace(" IntStack& operator=(IntStack&& other) noexcept;\n", "")
start = source.find("IntStack::IntStack(IntStack&& other)")
if start >= 0:
source = source[:start] + "void IntStack::swap" + source.split("void IntStack::swap")[1]
(base / "int_stack.h").write_text(header, encoding="utf-8")
(base / "int_stack.cpp").write_text(source, encoding="utf-8")
return base.relative_to(HERE)
def lab_tests(self, lab_dir, test_class):
env = dict(os.environ, LAB_DIR=str(lab_dir))
return run(["python3", "-m", "unittest", f"test_lab.{test_class}"], env=env)
def test_after_step_1_the_copy_demo_double_frees(self):
lab_dir = self.stage("step1", keep_copy=False, keep_move=False)
done = run(["make", "--no-print-directory", f"LAB_DIR={lab_dir}", "copy-demo"])
self.assertNotEqual(done.returncode, 0)
self.assertIn("attempting double-free", done.stdout)
self.assertEqual(self.lab_tests(lab_dir, "Step1Basics").returncode, 0)
self.assertNotEqual(self.lab_tests(lab_dir, "Step3Copy").returncode, 0)
def test_after_step_3_moving_still_reserves(self):
lab_dir = self.stage("step3", keep_copy=True, keep_move=False)
self.assertEqual(self.lab_tests(lab_dir, "Step3Copy").returncode, 0)
done = self.lab_tests(lab_dir, "Step4Move")
self.assertNotEqual(done.returncode, 0)
def test_the_starter_fails_every_step(self):
run(["make", "--no-print-directory", "LAB_DIR=lab", "lab"])
for test_class, step in LAB_TESTS.items():
with self.subTest(step=step):
self.assertNotEqual(self.lab_tests("lab", test_class).returncode, 0)
def test_the_starter_fails_almost_every_test(self):
env = dict(os.environ, LAB_DIR="lab")
done = run(["python3", "-m", "unittest", "test_lab"], env=env)
self.assertNotEqual(done.returncode, 0)
self.assertIn("failures=28", done.stdout)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.
Los programas se compilan y corren con sanitizers: una fuga o un acceso inválido hace
fallar la prueba, aunque el resultado parezca correcto.
"""
import os
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
OUT = HERE / "build" / LAB_DIR
SAMPLES = pathlib.Path("lab") / "samples"
AVAILABLE = (HERE / LAB_DIR).is_dir()
def short_report(text):
"""Deja lo útil de un informe: el error y las líneas de los archivos propios."""
useful = [line.strip() for line in text.splitlines()
if "ERROR:" in line or "SUMMARY:" in line or "runtime error" in line
or "Assertion" in line or "falta implementar" in line
or re.search(r"(int_stack|stack|brackets|test_\w+)\.(h|cpp):\d+", line)]
return "\n".join(useful[:12]) or text.strip()
def build(target):
done = subprocess.run(
["make", "--no-print-directory", f"LAB_DIR={LAB_DIR}", f"build/{LAB_DIR}/{target}"],
cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=300,
)
if done.returncode != 0:
raise AssertionError(f"no compila {target}:\n{done.stdout}")
def run_program(program, *args):
return subprocess.run(
[str(OUT / program), *args], cwd=HERE, text=True, encoding="utf-8", errors="replace",
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
@unittest.skipUnless(AVAILABLE, f"no existe {LAB_DIR}/")
class NamedTests(unittest.TestCase):
program = None
@classmethod
def setUpClass(cls):
if cls.program:
build(cls.program)
def check(self, name):
done = run_program(self.program, name)
self.assertEqual(done.returncode, 0, short_report(done.stdout))
class Step1Basics(NamedTests):
"""Paso 1: constructor, destructor y operaciones."""
program = "test_int_stack"
def test_lifo_order(self):
self.check("basic")
def test_reserves_once(self):
self.check("reserves_once")
def test_zero_capacity(self):
self.check("zero_capacity")
class Step3Copy(NamedTests):
"""Paso 3: copia y autoasignación."""
program = "test_int_stack"
def test_copy_constructor(self):
self.check("copy_constructor")
def test_copy_assignment(self):
self.check("copy_assignment")
def test_self_assignment(self):
self.check("self_assignment")
class Step4Move(NamedTests):
"""Paso 4: mover no reserva memoria."""
program = "test_int_stack"
def test_move_constructor(self):
self.check("move_constructor")
def test_move_assignment(self):
self.check("move_assignment")
def test_self_move(self):
self.check("self_move")
def test_moved_from_is_reusable(self):
self.check("moved_from_is_reusable")
def test_noexcept(self):
self.check("noexcept")
class Step5Template(NamedTests):
"""Paso 5: el template, con int y con std::string."""
program = "test_stack"
def test_int_lifo(self):
self.check("int_lifo")
def test_string_lifo(self):
self.check("string_lifo")
def test_zero_capacity(self):
self.check("zero_capacity")
def test_random_int(self):
self.check("random_int")
def test_random_string(self):
self.check("random_string")
def test_copies_are_independent(self):
self.check("copies_are_independent")
def test_move_does_not_copy(self):
self.check("move_does_not_copy")
def test_noexcept(self):
self.check("noexcept")
def test_plain_struct(self):
self.check("plain_struct")
@unittest.skipUnless(AVAILABLE, f"no existe {LAB_DIR}/")
class Step6Brackets(unittest.TestCase):
"""Paso 6: el verificador, sobre archivos de verdad."""
@classmethod
def setUpClass(cls):
build("check_brackets")
def expect(self, sample, code, message):
path = str(SAMPLES / sample)
done = run_program("check_brackets", path)
self.assertEqual(done.stdout.strip(), f"{path}{message}", short_report(done.stdout))
self.assertEqual(done.returncode, code)
def test_good_file_with_comments_and_strings(self):
self.expect("good.cpp", 0, ": ok")
def test_empty_file(self):
self.expect("empty.cpp", 0, ": ok")
def test_extra_close(self):
self.expect("extra_close.cpp", 1, ":2: ')' sin abrir")
def test_mismatch(self):
self.expect("mismatch.cpp", 1, ":6: ')' no cierra el '{' de la línea 4")
def test_unclosed(self):
self.expect("unclosed.cpp", 1, ":1: '{' sin cerrar")
def test_reports_only_the_first_error(self):
self.expect("first_of_two.cpp", 1, ":2: ']' no cierra el '(' de la línea 2")
def test_its_own_source_code(self):
for name in ["check_brackets.cpp", "code_text.cpp", "code_text.h", "brackets.h"]:
path = f"lab/{name}"
done = run_program("check_brackets", path)
self.assertEqual(done.stdout.strip(), f"{path}: ok", short_report(done.stdout))
def test_deep_nesting_does_not_overflow(self):
deep = HERE / "build" / f"deep_{LAB_DIR}.cpp"
deep.write_text("(" * 5000 + ")" * 5000, encoding="utf-8")
done = run_program("check_brackets", str(deep))
self.assertEqual(done.returncode, 0, short_report(done.stdout))
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-02.md"
SLIDES = ROOT / "slides" / "week-02.tex"
# Antes de un bloque cuya salida cambia con el sistema. No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un informe de ASan o de valgrind. 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=60,
)
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=60)
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()
folders = [HERE, HERE / "errors", HERE / "errors" / "split_template", HERE / "lab", HERE / "lab" / "tests"]
sources = [path for folder in folders for pattern in ("*.c", "*.cpp", "*.h") for path in folder.glob(pattern)]
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, 3)
@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, 5)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for language in ("c", "cpp"):
for lines, _ in note_blocks(language):
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(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()// El contrato de la pila, escrito una sola vez, sirve para cualquier implementación.
#include <cassert>
#include <random>
#include <stack>
#include <string>
#include "check.h"
#include "vector_stack.h"
template <typename S>
void last_in_first_out() {
S stack;
assert(stack.empty());
stack.push(1);
stack.push(2);
assert(stack.top() == 2);
stack.pop();
assert(stack.top() == 1);
stack.pop();
assert(stack.empty());
}
template <typename S>
void same_as_library(int seed) {
std::mt19937 random(seed);
std::uniform_int_distribution<int> coin(0, 2);
S stack;
std::stack<int> reference;
for (int i = 0; i < 5000; i++) {
if (coin(random) != 0 || reference.empty()) {
stack.push(i);
reference.push(i);
} else {
stack.pop();
reference.pop();
}
stack.check_invariants();
assert(stack.empty() == reference.empty());
assert(stack.size() == reference.size());
if (!reference.empty()) {
assert(stack.top() == reference.top());
}
}
}
int main() {
run_test("último en entrar, primero en salir", [] { last_in_first_out<VectorStack<int>>(); });
run_test("igual que std::stack", [] { same_as_library<VectorStack<int>>(2026); });
run_test("con std::string", [] {
VectorStack<std::string> words;
words.push("hola");
words.push("chau");
assert(words.top() == "chau");
});
return 0;
}Laboratorio
Archivos de partida del laboratorio de la semana.
#include "brackets.h"
#include "stack.h"
BracketReport check_brackets(const std::string&) {
// Paso 6. Cada apertura debe guardar su carácter y su línea, así que piensen qué tipo T
// usar en Stack<T>. ¿Qué capacidad garantiza que la pila nunca se llene, sea cual sea el texto?
todo("check_brackets");
}// Verificador de paréntesis, corchetes y llaves. Paso 6 del laboratorio.
#ifndef BRACKETS_H
#define BRACKETS_H
#include <string>
struct BracketReport {
int line; // Línea del primer error, desde 1. Es 0 si no hay errores.
std::string message; // Vacío si no hay errores.
};
// Busca el primer error de ( ) [ ] { } en un texto. Todo lo demás se ignora.
// Hay tres errores posibles, con estos mensajes exactos:
// ')' sin abrir en la línea del cierre.
// '}' no cierra el '(' de la línea 3 en la línea del cierre.
// '(' sin cerrar al terminar el texto, en la línea de la
// última apertura pendiente, la del tope.
BracketReport check_brackets(const std::string& text);
#endif// Revisa archivos de código: build/lab/check_brackets archivo1 archivo2 ...
// Devuelve 0 si todos están bien, 1 si alguno tiene un error y 2 si no se pudo leer alguno.
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include "brackets.h"
#include "code_text.h"
int main(int argc, char* argv[]) {
int status = 0;
for (int i = 1; i < argc; i++) {
std::ifstream file(argv[i]);
if (!file) {
std::cerr << argv[i] << ": no se pudo abrir\n";
status = 2;
continue;
}
std::ostringstream contents;
contents << file.rdbuf();
BracketReport report = check_brackets(blank_comments_and_literals(contents.str()));
if (report.line == 0) {
std::cout << argv[i] << ": ok\n";
} else {
std::cout << argv[i] << ":" << report.line << ": " << report.message << "\n";
if (status == 0) {
status = 1;
}
}
}
return status;
}#include "code_text.h"
#include <cstddef>
namespace {
// Blanquea desde start hasta la comilla que cierra. Devuelve dónde sigue el texto.
std::size_t blank_literal(std::string& text, std::size_t start) {
char quote = text[start];
std::size_t i = start + 1;
while (i < text.size() && text[i] != quote && text[i] != '\n') {
i += text[i] == '\\' ? 2 : 1;
}
std::size_t end = i < text.size() ? i + 1 : text.size();
for (std::size_t j = start; j < end; j++) {
if (text[j] != '\n') {
text[j] = ' ';
}
}
return end;
}
// Blanquea un comentario de bloque que empieza en start. Devuelve dónde sigue el texto.
std::size_t blank_block_comment(std::string& text, std::size_t start) {
std::size_t close = text.find("*/", start + 2);
std::size_t end = close == std::string::npos ? text.size() : close + 2;
for (std::size_t j = start; j < end; j++) {
if (text[j] != '\n') {
text[j] = ' ';
}
}
return end;
}
} // namespace
std::string blank_comments_and_literals(const std::string& code) {
std::string text = code;
std::size_t i = 0;
while (i < text.size()) {
if (text.compare(i, 2, "//") == 0) {
while (i < text.size() && text[i] != '\n') {
text[i] = ' ';
i++;
}
} else if (text.compare(i, 2, "/*") == 0) {
i = blank_block_comment(text, i);
} else if (text[i] == '"' || text[i] == '\'') {
i = blank_literal(text, i);
} else {
i++;
}
}
return text;
}// Prepara un archivo de código para el verificador.
#ifndef CODE_TEXT_H
#define CODE_TEXT_H
#include <string>
// Reemplaza por espacios los comentarios, las cadenas y los caracteres literales de C y C++.
// Conserva los saltos de línea, así que los números de línea no cambian.
// Límite conocido: no entiende las cadenas crudas de C++, como R"(...)".
std::string blank_comments_and_literals(const std::string& code);
#endif#include "int_stack.h"
#include "todo.h"
IntStack::IntStack(std::size_t) {
todo("IntStack::IntStack");
}
IntStack::~IntStack() {
todo("IntStack::~IntStack");
}
void IntStack::push(int) {
todo("IntStack::push");
}
void IntStack::pop() {
todo("IntStack::pop");
}
int IntStack::top() const {
todo("IntStack::top");
}
bool IntStack::empty() const {
todo("IntStack::empty");
}
bool IntStack::full() const {
todo("IntStack::full");
}
std::size_t IntStack::size() const {
todo("IntStack::size");
}
std::size_t IntStack::capacity() const {
todo("IntStack::capacity");
}// Pila de enteros sobre un arreglo de capacidad fija. Pasos 1, 3 y 4 del laboratorio.
#ifndef INT_STACK_H
#define INT_STACK_H
#include <cstddef>
// Invariantes:
// 1. size_ <= capacity_.
// 2. data_ == nullptr si y solo si capacity_ == 0.
// 3. Los elementos están en data_[0], ..., data_[size_ - 1]. El tope es data_[size_ - 1].
class IntStack {
public:
// Paso 1. Reserva con new[] espacio para capacity enteros. La pila empieza vacía.
explicit IntStack(std::size_t capacity);
~IntStack();
// Paso 3: declaren aquí el constructor de copia y la asignación de copia.
// Paso 4: declaren aquí el constructor y la asignación de movimiento, con noexcept.
// Una pila desde la que se movió queda vacía y con capacidad 0.
// Paso 1. Las operaciones.
void push(int value); // Requiere !full().
void pop(); // Requiere !empty().
int top() const; // Requiere !empty().
bool empty() const;
bool full() const;
std::size_t size() const;
std::size_t capacity() const;
private:
int* data_ = nullptr;
std::size_t capacity_ = 0;
std::size_t size_ = 0;
};
#endif// Pila genérica sobre un arreglo de capacidad fija. Paso 5 del laboratorio.
#ifndef STACK_H
#define STACK_H
#include <cstddef>
#include "todo.h"
// Por ahora el arreglo se pide con new T[capacity], así que T debe tener un constructor
// sin argumentos, y se debe poder asignar. Stack<T> no compila con otros tipos.
//
// Invariantes:
// 1. size_ <= capacity_.
// 2. data_ == nullptr si y solo si capacity_ == 0.
// 3. Los elementos están en data_[0], ..., data_[size_ - 1]. El tope es data_[size_ - 1].
template <typename T>
class Stack {
public:
explicit Stack(std::size_t) {
todo("Stack::Stack");
}
~Stack() {
todo("Stack::~Stack");
}
// Las cuatro que faltan de la regla de cinco. Una pila desde la que se movió queda
// vacía y con capacidad 0, y cumple los invariantes.
void push(T) { // Requiere !full().
todo("Stack::push");
}
void pop() { // Requiere !empty().
todo("Stack::pop");
}
T& top() { // Requiere !empty().
todo("Stack::top");
}
const T& top() const { // Requiere !empty().
todo("Stack::top");
}
bool empty() const {
todo("Stack::empty");
}
bool full() const {
todo("Stack::full");
}
std::size_t size() const {
todo("Stack::size");
}
std::size_t capacity() const {
todo("Stack::capacity");
}
// Comprueba con assert los invariantes de arriba.
void check_invariants() const {
todo("Stack::check_invariants");
}
private:
T* data_ = nullptr;
std::size_t capacity_ = 0;
std::size_t size_ = 0;
};
#endif// Marca lo que falta escribir. Borren cada llamada a todo cuando escriban esa función.
#ifndef TODO_H
#define TODO_H
#include <cstdlib>
#include <iostream>
[[noreturn]] inline void todo(const char* what) {
std::cerr << "falta implementar: " << what << "\n";
std::abort();
}
#endifint total = 3;
int other = (1 + 2));
int last = 0;void f(void) {
g(1];
h(2];
}// Comentario con ( sin cerrar, que no cuenta.
#include <stdio.h>
/* Otro comentario, de varias líneas:
} ] )
*/
int main(void) {
int numbers[3] = {1, 2, 3};
char open = '(';
printf("cadena con ] y con \" adentro %d %c\n", numbers[0], open);
return 0;
}int main(void) {
int values[3] = {1, 2, 3};
int total = (values[0] + values[1];
if (total > 0) {
return 1;
)
return 0;
}int main(void) {
if (1) {
return 0;
}
int values[2] = {1, 2};
while (values[0] > 0) {
values[0]--;
return 0;
}// Cuenta las reservas con new[] y las liberaciones con delete[] de todo el programa.
// Reemplaza esos operadores globales, así que se incluye en un solo .cpp de cada programa.
#ifndef ALLOC_COUNT_H
#define ALLOC_COUNT_H
#include <cstddef>
#include <new>
inline std::size_t& array_news() {
static std::size_t count = 0;
return count;
}
inline std::size_t& array_deletes() {
static std::size_t count = 0;
return count;
}
void* operator new[](std::size_t bytes) {
++array_news();
return ::operator new(bytes);
}
void operator delete[](void* block) noexcept {
if (block != nullptr) {
++array_deletes();
}
::operator delete(block);
}
void operator delete[](void* block, std::size_t) noexcept {
if (block != nullptr) {
++array_deletes();
}
::operator delete(block);
}
#endif// Paso 2 del laboratorio: copia una pila. Sin constructor de copia propio, ASan reclama.
#include <iostream>
#include "int_stack.h"
int main() {
IntStack a(4);
a.push(1);
a.push(2);
IntStack b = a;
b.push(3);
b.pop();
b.pop();
b.push(50);
std::cout << "a.top() = " << a.top() << "\n";
std::cout << "b.top() = " << b.top() << "\n";
return 0;
}// Pruebas públicas de IntStack. Cada una se corre aparte: test_int_stack nombre.
#include <cassert>
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include "alloc_count.h"
#include "int_stack.h"
namespace {
void test_basic() {
IntStack s(3);
assert(s.empty() && !s.full());
assert(s.size() == 0 && s.capacity() == 3);
s.push(1);
s.push(2);
s.push(3);
assert(s.full() && !s.empty() && s.size() == 3);
assert(s.top() == 3);
s.pop();
assert(s.top() == 2 && !s.full());
s.pop();
s.pop();
assert(s.empty());
s.push(9);
assert(s.top() == 9);
}
void test_reserves_once() {
std::size_t before = array_news();
{
IntStack s(100);
for (int i = 0; i < 100; i++) {
s.push(i);
}
assert(array_news() == before + 1);
}
assert(array_news() - before == 1);
}
void test_zero_capacity() {
IntStack s(0);
assert(s.empty() && s.full() && s.capacity() == 0);
}
void test_copy_constructor() {
IntStack a(4);
a.push(1);
a.push(2);
IntStack b = a;
assert(b.size() == 2 && b.capacity() == 4 && b.top() == 2);
b.pop();
b.push(50);
assert(a.top() == 2 && b.top() == 50);
a.pop();
assert(a.top() == 1 && b.size() == 2);
}
void test_copy_assignment() {
IntStack a(4);
a.push(1);
a.push(2);
IntStack b(1);
b.push(7);
b = a;
assert(b.size() == 2 && b.capacity() == 4 && b.top() == 2);
b.pop();
b.push(50);
assert(a.top() == 2 && b.top() == 50);
}
void test_self_assignment() {
IntStack a(4);
a.push(1);
a.push(2);
IntStack& same = a;
a = same;
assert(a.size() == 2 && a.capacity() == 4 && a.top() == 2);
a.pop();
assert(a.top() == 1);
}
void test_move_constructor() {
IntStack a(4);
a.push(1);
a.push(2);
std::size_t before = array_news();
IntStack b = std::move(a);
assert(array_news() == before);
assert(b.size() == 2 && b.capacity() == 4 && b.top() == 2);
assert(a.empty() && a.capacity() == 0);
}
void test_move_assignment() {
IntStack a(4);
a.push(1);
a.push(2);
IntStack b(9);
b.push(7);
std::size_t before = array_news();
b = std::move(a);
assert(array_news() == before);
assert(b.size() == 2 && b.capacity() == 4 && b.top() == 2);
assert(a.empty() && a.capacity() == 0);
}
void test_self_move() {
IntStack a(4);
a.push(1);
IntStack& same = a;
a = std::move(same);
assert(a.size() == 1 && a.capacity() == 4 && a.top() == 1);
}
void test_moved_from_is_reusable() {
IntStack a(4);
a.push(1);
IntStack b = std::move(a);
assert(a.full());
a = b;
assert(a.size() == 1 && a.top() == 1);
a.push(2);
assert(a.top() == 2 && b.size() == 1);
}
void test_noexcept() {
IntStack a(1);
IntStack b = std::move(a);
assert(a.capacity() == 0 && b.capacity() == 1);
assert(std::is_nothrow_move_constructible_v<IntStack>);
assert(std::is_nothrow_move_assignable_v<IntStack>);
}
} // namespace
int main(int argc, char* argv[]) {
std::string name = argc > 1 ? argv[1] : "";
if (name == "basic") test_basic();
else if (name == "reserves_once") test_reserves_once();
else if (name == "zero_capacity") test_zero_capacity();
else if (name == "copy_constructor") test_copy_constructor();
else if (name == "copy_assignment") test_copy_assignment();
else if (name == "self_assignment") test_self_assignment();
else if (name == "move_constructor") test_move_constructor();
else if (name == "move_assignment") test_move_assignment();
else if (name == "self_move") test_self_move();
else if (name == "moved_from_is_reusable") test_moved_from_is_reusable();
else if (name == "noexcept") test_noexcept();
else {
std::cerr << "prueba desconocida: " << name << "\n";
return 2;
}
return 0;
}// Pruebas públicas de Stack<T>. Cada una se corre aparte: test_stack nombre.
#include <cassert>
#include <iostream>
#include <random>
#include <stack>
#include <string>
#include <type_traits>
#include <utility>
#include "alloc_count.h"
#include "stack.h"
namespace {
// Cuenta cuántas veces se copia un elemento.
class Counted {
public:
Counted() = default;
explicit Counted(int value) : value_(value) {}
Counted(const Counted& other) : value_(other.value_) {
copies()++;
}
Counted& operator=(const Counted& other) {
value_ = other.value_;
copies()++;
return *this;
}
Counted(Counted&&) noexcept = default;
Counted& operator=(Counted&&) noexcept = default;
~Counted() = default;
int value() const {
return value_;
}
static int& copies() {
static int count = 0;
return count;
}
private:
int value_ = 0;
};
void test_int_lifo() {
Stack<int> s(3);
s.check_invariants();
assert(s.empty() && !s.full() && s.capacity() == 3);
s.push(1);
s.push(2);
s.push(3);
s.check_invariants();
assert(s.full() && s.size() == 3 && s.top() == 3);
s.pop();
assert(s.top() == 2);
s.top() = 20;
assert(s.top() == 20);
const Stack<int>& view = s;
assert(view.top() == 20);
}
void test_string_lifo() {
Stack<std::string> s(2);
s.push("hola");
s.push("un texto bastante largo para que el string pida memoria");
assert(s.top().size() > 20);
s.pop();
assert(s.top() == "hola");
s.check_invariants();
}
void test_zero_capacity() {
Stack<std::string> s(0);
s.check_invariants();
assert(s.empty() && s.full());
}
template <typename T, typename Make>
void same_as_library(unsigned seed, Make make) {
std::mt19937 random(seed);
std::uniform_int_distribution<int> action(0, 3);
const std::size_t capacity = 200;
Stack<T> s(capacity);
std::stack<T> reference;
for (int i = 0; i < 5000; i++) {
int choice = action(random);
if (choice == 3) {
// Copias y movimientos, entre operaciones normales.
Stack<T> copy = s;
copy.check_invariants();
s = std::move(copy);
copy.check_invariants();
} else if ((choice == 0 && !reference.empty()) || reference.size() == capacity) {
s.pop();
reference.pop();
} else {
T value = make(i);
s.push(value);
reference.push(value);
}
s.check_invariants();
assert(s.size() == reference.size());
assert(s.empty() == reference.empty());
assert(s.full() == (reference.size() == capacity));
if (!reference.empty()) {
assert(s.top() == reference.top());
}
}
}
void test_random_int() {
same_as_library<int>(2026, [](int i) { return i; });
}
void test_random_string() {
same_as_library<std::string>(2026, [](int i) { return "elemento numero " + std::to_string(i); });
}
void test_copies_are_independent() {
Stack<std::string> a(3);
a.push("uno");
Stack<std::string> b = a;
b.top() = "dos";
assert(a.top() == "uno" && b.top() == "dos");
Stack<std::string> c(1);
c = a;
c.push("tres");
assert(a.size() == 1 && c.size() == 2 && c.capacity() == 3);
Stack<std::string>& same = a;
a = same;
assert(a.top() == "uno" && a.capacity() == 3);
a.check_invariants();
}
void test_move_does_not_copy() {
Stack<Counted> a(5);
a.push(Counted(1));
a.push(Counted(2));
std::size_t news = array_news();
int copies = Counted::copies();
Stack<Counted> b = std::move(a);
assert(array_news() == news);
assert(Counted::copies() == copies);
assert(b.top().value() == 2 && b.size() == 2);
assert(a.empty() && a.capacity() == 0);
a.check_invariants();
Stack<Counted> c(1);
news = array_news();
c = std::move(b);
assert(array_news() == news);
assert(Counted::copies() == copies);
assert(c.top().value() == 2 && b.capacity() == 0);
b.check_invariants();
Stack<Counted>& same = c;
c = std::move(same);
assert(c.top().value() == 2 && c.size() == 2);
c.check_invariants();
}
void test_noexcept() {
Stack<std::string> a(1);
Stack<std::string> b = std::move(a);
assert(a.capacity() == 0 && b.capacity() == 1);
assert(std::is_nothrow_move_constructible_v<Stack<std::string>>);
assert(std::is_nothrow_move_assignable_v<Stack<std::string>>);
}
struct Pair {
char symbol;
int line;
};
void test_plain_struct() {
Stack<Pair> s(2);
s.push(Pair{'(', 4});
assert(s.top().symbol == '(' && s.top().line == 4);
s.pop();
assert(s.empty());
}
} // namespace
int main(int argc, char* argv[]) {
std::string name = argc > 1 ? argv[1] : "";
if (name == "int_lifo") test_int_lifo();
else if (name == "string_lifo") test_string_lifo();
else if (name == "zero_capacity") test_zero_capacity();
else if (name == "random_int") test_random_int();
else if (name == "random_string") test_random_string();
else if (name == "copies_are_independent") test_copies_are_independent();
else if (name == "move_does_not_copy") test_move_does_not_copy();
else if (name == "noexcept") test_noexcept();
else if (name == "plain_struct") test_plain_struct();
else {
std::cerr << "prueba desconocida: " << name << "\n";
return 2;
}
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 2: ownership y templates. 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 copy-demo corre la prueba del paso 2 del laboratorio, que copia una pila
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-lab prueba su laboratorio; falla hasta que lo terminen
# 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.
CC = gcc
CXX = g++
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BUILD = build
LAB_DIR ?= lab
OUT = $(BUILD)/$(LAB_DIR)
C_PROGRAMS = owner_in_c max_void
PROGRAMS = new_basics scope_trace array_constructs special_members implicit_moves vector_growth \
library_moved_from max_of postfix no_default_vector buffer_test sorted_ints_test \
vector_stack_test
BUFFER_PROGRAMS = copy_demo move_demo int_buffer_test
LAB_PROGRAMS = test_int_stack copy_demo test_stack check_brackets
.PHONY: all lab copy-demo test test-lab clean
all: $(C_PROGRAMS:%=$(BUILD)/%) $(PROGRAMS:%=$(BUILD)/%) $(BUFFER_PROGRAMS:%=$(BUILD)/%)
$(BUILD)/%: %.c | $(BUILD)
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/%: %.cpp $(wildcard *.h) | $(BUILD)
$(CXX) $(CXXFLAGS) $< -o $@
$(BUFFER_PROGRAMS:%=$(BUILD)/%): $(BUILD)/%: %.cpp int_buffer.cpp int_buffer.h alloc_count.h | $(BUILD)
$(CXX) $(CXXFLAGS) $< int_buffer.cpp -o $@
$(BUILD):
mkdir -p $@
# Lo que escriben ustedes está en $(LAB_DIR). Las pruebas y lo que les damos, siempre en lab/.
INT_STACK = $(LAB_DIR)/int_stack.h $(LAB_DIR)/int_stack.cpp
TEMPLATE = $(LAB_DIR)/stack.h
LAB_FLAGS = $(CXXFLAGS) -I$(LAB_DIR) -Ilab
$(OUT)/test_int_stack: lab/tests/test_int_stack.cpp lab/tests/alloc_count.h $(INT_STACK) | $(OUT)
$(CXX) $(LAB_FLAGS) lab/tests/test_int_stack.cpp $(LAB_DIR)/int_stack.cpp -o $@
$(OUT)/copy_demo: lab/tests/copy_demo.cpp $(INT_STACK) | $(OUT)
$(CXX) $(LAB_FLAGS) lab/tests/copy_demo.cpp $(LAB_DIR)/int_stack.cpp -o $@
$(OUT)/test_stack: lab/tests/test_stack.cpp lab/tests/alloc_count.h $(TEMPLATE) | $(OUT)
$(CXX) $(LAB_FLAGS) lab/tests/test_stack.cpp -o $@
$(OUT)/check_brackets: lab/check_brackets.cpp lab/code_text.cpp lab/code_text.h lab/brackets.h $(LAB_DIR)/brackets.cpp $(TEMPLATE) | $(OUT)
$(CXX) $(LAB_FLAGS) lab/check_brackets.cpp lab/code_text.cpp $(LAB_DIR)/brackets.cpp -o $@
$(OUT):
mkdir -p $@
lab: $(LAB_PROGRAMS:%=$(OUT)/%)
copy-demo: $(OUT)/copy_demo
$(OUT)/copy_demo
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
python3 -m unittest discover -p 'test_*.py'
test-lab: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab
clean:
rm -rf $(BUILD) __pycache__