Skip to content

Problem model

The problem is described by three value types you define: Input, Solution and Move. The framework imposes no base class and no member on them; the hooks below are optional and used only by the tools.

Contract

Type Requirements Notes
Input none built once, immutable while services are bound to it
Solution copyable, movable does not reference the Input
Move copyable does not reference a Solution; default-constructible if the explorer uses a cursor

Optional hooks

Each kind of hook may be written in three ways, tried in this order:

Hook Forms, in order Used by
read an Input static Input::read(std::istream&); read_input(std::type_identity<Input>, std::istream&) by ADL; operator>> on a default-constructed Input read_input, load_input, Session, TextUI
read a Solution static Solution::read(const Input&, std::istream&); read_solution(const Input&, std::istream&) by ADL; operator>> on Solution{input} read_solution, load_solution, Session, TextUI
write a Solution Solution::write(const Input&, std::ostream&) const; write_solution(const Input&, const Solution&, std::ostream&) by ADL; operator<< write_solution, save_solution, Session, TextUI
describe a value describe() const member; describe(const T&) by ADL; operator<<; a std::variant, or a union move, by what it holds describe, TextUI
read a cost read_cost(const Input&, std::string_view) by ADL; else cost::from_text read_cost, Session, TextUI, RunParameters (see Cost)
compare solutions the SolutionManager's equal(const Solution&, const Solution&); operator== on Solution (solution identity) the checks of easylocal::testing, Session::check_move_independence, TextUI, reactive tabu list
compare moves operator== on Move tests, Session and TextUI

The TextUI shows a Solution without a describe hook as its write hook writes it.

Reading and writing

<easylocal/app/io.hpp> reads and writes values through the hooks, in any program:

Function
read_input<Input>(std::istream&), load_input<Input>(path) an Input
read_solution<Solution>(input, std::istream&), load_solution<Solution>(input, path) a Solution of input
write_solution(input, solution, std::ostream&), save_solution(input, solution, path) writes a Solution
describe(value) -> std::string the text of a value, for people

The concepts readable_input<Input>, readable_solution<Input, Solution>, writable_solution<Input, Solution> and describable<T> tell whether a hook exists; has_describe<T> whether T has a describe of its own, member or free, not just operator<<. The functions throw std::runtime_error when a stream fails (or what a hook throws); the file functions report errors as std::runtime_error naming the file, for save_solution those of the final flush and close too. write_solution and describe are function objects, so the argument-dependent lookup of a problem's own hook never finds them.

Convenience bases

Two optional, non-virtual bases give a class what it needs to receive the Input and its parameters, so that every service does it the same way:

Base Header Gives
easylocal::input_base<Input> <easylocal/utils/input_base.hpp> input_type, a constructor from const Input& (the one from a temporary is deleted) and input()
easylocal::parameters_base<Parameters> <easylocal/config/parameters_base.hpp> parameters_type, which makes the class configurable, a constructor from the block and parameters()
class TourLength : public easylocal::input_base<Tsp>
{
public:
    using input_base::input_base;   // the constructor, which input() reads back

    double evaluate(const Tour& tour) const { /* ... input().distance ... */ }
};
  • Both are optional: a class that declares the same members itself does just as well, and a cost component that needs neither the Input nor parameters derives from nothing.
  • Inherit the constructor with using input_base::input_base; (or using parameters_base::parameters_base;). A class that derives data from the Input writes its own constructor instead and passes the Input on: explicit C(const Input& input) : input_base{input}, table_{build(input)} {}.
  • Derive from each at most once: a cost component with a co-located delta is one class, with one Input.
  • solution_manager_base and neighborhood_explorer_base derive from input_base, so the Input reaches a SolutionManager, an explorer and a cost component the same way.
  • parameters_base does not validate the block: the recipe that holds it validates it before every construction.
  • A class that takes both is built from the Input first, then the parameters, the order the recipes pass them in:

    class WeightedLength
        : public easylocal::input_base<Tsp>,
          public easylocal::parameters_base<WeightParameters>
    {
    public:
        WeightedLength(const Tsp& input, WeightParameters parameters)
            : input_base{input}, parameters_base{std::move(parameters)} {}
    };
    

Design choices

  • Plain values. Entities carry no references to each other, so a Solution can be copied, stored and compared freely, and a Move can be applied to any compatible Solution. Behaviour lives in services (SolutionManager, NeighborhoodExplorer), not in the values.
  • Immutable, borrowed Input. Services borrow the Input by const& for their whole lifetime; binding a temporary Input is rejected. Concurrent runs share only the Input.
  • Who receives the Input. Three kinds of code, three rules:
  • values (Input, Solution, Move) hold no Input;
  • services (SolutionManager, NeighborhoodExplorers, cost components, delta cost components) receive it once, in their constructor, when a runner or an app is bound, and keep it; their members take only the solution and the move;
  • hooks, free functions that are not services (read_solution, write_solution, read_cost), receive it as a parameter.

A service is built once per Input, so its constructor is where data derived from the instance is computed, such as the conflicts of each exam or a neighbour list: once per bind, owned by the service that uses it, and kept out of the Input, which stays the instance as read. Passing the Input to every call instead would leave such data nowhere but in the Input itself or in a mutable cache.

  • Structural vs. feasible. Validity (see SolutionManager) is about the representation; constraint violations are part of the cost.