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, delete y 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::move no 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 con void * 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_invariants para 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:

  1. Escribir IntStack, sobre un arreglo de capacidad fija que se pide con new[] en el constructor, y su destructor.
  2. Correr la prueba dada que copia una pila, leer el error de ASan y explicar con un dibujo por qué ocurre.
  3. 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

  1. ¿Qué pasa si una clase con un int * y un destructor que hace delete[] se copia con la copia por defecto? Dibújenlo.
  2. ¿Qué cinco funciones nombra la regla de cinco? ¿Por qué aparecen juntas?
  3. ¿En qué estado debe quedar un objeto después de que se mueve desde él?
  4. ¿Por qué el código de un template se escribe en el encabezado?
  5. ¿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.

alloc_count.hDescargar alloc_count.h
// 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
array_constructs.cppDescargar array_constructs.cpp
// 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;
}
buffer.hDescargar buffer.h
// 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
check.hDescargar check.h
// 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
copy_demo.cppDescargar copy_demo.cpp
// 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;
}
implicit_moves.cppDescargar implicit_moves.cpp
// 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;
}
int_buffer.cppDescargar int_buffer.cpp
#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));
}
int_buffer.hDescargar int_buffer.h
// 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
library_moved_from.cppDescargar library_moved_from.cpp
// 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;
}
max_of.cppDescargar max_of.cpp
// 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;
}
max_void.cDescargar max_void.c
// 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;
}
move_demo.cppDescargar move_demo.cpp
// 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_basics.cppDescargar new_basics.cpp
// 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;
}
no_default_vector.cppDescargar no_default_vector.cpp
// 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;
}
owner_in_c.cDescargar owner_in_c.c
// 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;
}
postfix.cppDescargar postfix.cpp
// 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;
}
scope_trace.cppDescargar scope_trace.cpp
// 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;
}
sorted_ints.hDescargar sorted_ints.h
// 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
special_members.cppDescargar special_members.cpp
// 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;
}
traced.hDescargar traced.h
// 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
vector_growth.cppDescargar vector_growth.cpp
// 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;
}
vector_stack.hDescargar vector_stack.h
// 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_;
};

#endif

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

errors/ambiguous_assign.cppDescargar errors/ambiguous_assign.cpp
// 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;
}
errors/broken_sorted.cppDescargar errors/broken_sorted.cpp
// 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;
}
errors/move_deletes_copy.cppDescargar errors/move_deletes_copy.cpp
// 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;
}
errors/new_delete_mismatch.cppDescargar errors/new_delete_mismatch.cpp
// 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;
}
errors/no_default.cppDescargar errors/no_default.cpp
// 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;
}
errors/no_operator.cppDescargar errors/no_operator.cpp
// 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;
}
errors/pessimizing_move.cppDescargar errors/pessimizing_move.cpp
// 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;
}
errors/self_assign.cppDescargar errors/self_assign.cpp
// 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;
}
errors/shallow_assign.cppDescargar errors/shallow_assign.cpp
// 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;
}
errors/shallow_copy.cppDescargar errors/shallow_copy.cpp
// 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;
}
errors/shallow_move.cppDescargar errors/shallow_move.cpp
// 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;
}
errors/template_wrong_type.cppDescargar errors/template_wrong_type.cpp
// 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;
}
errors/use_after_move.cppDescargar errors/use_after_move.cpp
// 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;
}
errors/void_wrong_compare.cDescargar errors/void_wrong_compare.c
// 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;
}
errors/split_template/box.cppDescargar errors/split_template/box.cpp
// 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_;
}
errors/split_template/box.hDescargar errors/split_template/box.h
// 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
errors/split_template/main.cppDescargar errors/split_template/main.cpp
// 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.

buffer_test.cppDescargar buffer_test.cpp
// 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;
}
int_buffer_test.cppDescargar int_buffer_test.cpp
// 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;
}
sorted_ints_test.cppDescargar sorted_ints_test.cpp
// 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;
}
test_examples.pyDescargar test_examples.py
"""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()
test_lab.pyDescargar test_lab.py
"""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()
test_notes.pyDescargar test_notes.py
"""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()
vector_stack_test.cppDescargar vector_stack_test.cpp
// 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.

lab/brackets.cppDescargar lab/brackets.cpp
#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");
}
lab/brackets.hDescargar lab/brackets.h
// 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
lab/check_brackets.cppDescargar lab/check_brackets.cpp
// 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;
}
lab/code_text.cppDescargar lab/code_text.cpp
#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;
}
lab/code_text.hDescargar lab/code_text.h
// 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
lab/int_stack.cppDescargar lab/int_stack.cpp
#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");
}
lab/int_stack.hDescargar lab/int_stack.h
// 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
lab/stack.hDescargar lab/stack.h
// 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
lab/todo.hDescargar lab/todo.h
// 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();
}

#endif
lab/samples/empty.cppDescargar lab/samples/empty.cpp
lab/samples/extra_close.cppDescargar lab/samples/extra_close.cpp
int total = 3;
int other = (1 + 2));
int last = 0;
lab/samples/first_of_two.cppDescargar lab/samples/first_of_two.cpp
void f(void) {
    g(1];
    h(2];
}
lab/samples/good.cppDescargar lab/samples/good.cpp
// 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;
}
lab/samples/mismatch.cppDescargar lab/samples/mismatch.cpp
int main(void) {
    int values[3] = {1, 2, 3};
    int total = (values[0] + values[1];
    if (total > 0) {
        return 1;
    )
    return 0;
}
lab/samples/unclosed.cppDescargar lab/samples/unclosed.cpp
int main(void) {
    if (1) {
        return 0;
    }
    int values[2] = {1, 2};
    while (values[0] > 0) {
        values[0]--;
    return 0;
}
lab/tests/alloc_count.hDescargar lab/tests/alloc_count.h
// 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
lab/tests/copy_demo.cppDescargar lab/tests/copy_demo.cpp
// 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;
}
lab/tests/test_int_stack.cppDescargar lab/tests/test_int_stack.cpp
// 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;
}
lab/tests/test_stack.cppDescargar lab/tests/test_stack.cpp
// 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.

MakefileDescargar Makefile
# 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__