Callbacks e uma solução em C++

O resolvedor de PLI Xpress possui algumas callbacks que são chamadas em pontos específicos do código. Existem callbacks que são chamadas cada vez que a relaxação linear é resolvida em um nó do branch-and-bound, logo depois de decidir qual variável sofrerá branching, quando encontra uma solução inteira, etc.

As callbacks nada mais são do que ponteiros para funções providas pelo usuário. Por exemplo, a callback que será chamada logo depois de uma variável ser escolhida para sofrer branching possui a seguinte forma:

void (*fucb)(XPRSprob my_prob, void *my_object, int *entity, int *up, double *estdeg)

Ou seja, precisamos passar uma função que recebe my_prob, que é um ponteiro para a estrutura do modelo atual; my_object é um ponteiro para um objeto fornecido pelo usuário; entity é o identificador da variável que vai sofrer branch (geralmente é o número da coluna do modelo); up indica se a variável será arredondada para cima ou para baixo; já estdeg (estimated degradation) indica quanto a função objetivo vai piorar ao fazer o arredondamento.

Para setar essa callback, devemos chamar a seguinte função da API do XPRESS:

int XPRSsetcbchgbranch(XPRSprob prob, void (*fucb)(XPRSprob my_prob, void *my_object, int *entity, int *up, double *estdeg), void *object);

Note que é aqui que devemos informar o endereço da estrutura que será passada para nossa função através de my_object.

Vamos a um exemplo minimal.

void observer(XPRSprob my_prob, 
              void *my_object, 
              int *entity,
              int *up, 
              double *estdeg){
    printf("%d\n", *my_object);
}
...
int obj = 3;
int XPRSsetcbchgbranch(prob, observer, &obj);

Aqui eu passo a variável obj ao setar a callback. Logo, toda vez que a função for chamada, ela imprimirá 3.

E se eu quiser fazer algo mais complicado, como por exemplo imprimir o nome da variável? Eu poderia definir um mapa que leva um índice de coluna no nome da variável.

Porém, é mais eficiente fazer esse mapeamento através de uma função. Minha variável tem o nome x(i,j) e a coluna é dada por col = i*n + j, onde n é o número de variáveis j. Para fazer o mapeamento reverso eu faço: i = col/n e j = col%n.

A função teria a seguinte cara:

void rev_map(int col, int *i, int *j){
    *i = col/n;
    *j = col%n;
}

A função foo a seguir simula a função de callback, que receberá um elemento *void, que nesse caso é um ponteiro para função.

int foo(void *obj){
    void (*ptr)(int col, int *i, int *j)
        = (void(*)(int col, int *i, int *j)) obj;
    int i, j;
    
    ptr(5, &i, &j);
    cout << "i: " << i << ", j: " << j << endl;
}

int main (){
    foo((void*) rev_map);
}

O único porém é que a função depende da variável n. Como fazer? Poderíamos fazer igual à API do XPRESS, colocando um parâmetro a mais em rev_map, e passando um parâmetro a mais para foo.

void rev_map(int col, int *i, int *j, void *ext){
    int *n = (int*)(ext);
    *i = col/(*n);
    *j = col%(*n);
}

int foo(void *obj, void *ext){
    void (*ptr)(int col, int *i, int *j, void *ext)
        = (void(*)(int col, int *i, int *j, void *ext)) obj;
    int i, j;
    
    ptr(5, &i, &j, ext);
    std::cout << "i: " << i << ", j: " << j << std::endl;
}

int main (){
    int n = 3;
    foo((void*) rev_map, &n);
}

O negócio começa a ficar feio…

Podemos usar uma classe para simplificar.

class rev_map {
    int n;
public:
    rev_map(int _n) : n(_n) {};
    void operator() (int col, int *i, int *j){
        *i = col / n;
        *j = col % n;
    }
};

A vantagem é que ela fornece uma função com estado interno (o membro n). Aí não precisa ficar passando o n como parâmetro pra lá e pra cá. Como essa classe é só um wrapper para uma função, achei mais conveniente fazer a sobrecarga do operator () do que definir um método. Esse tipo de classe também é conhecida como functor.

Objetos dessa classe são quase como funções, já que você pode invocá-los diretamente como no exemplo:

rev_map obj;
obj(col, &i, &j);

O resto do código fica:

int foo(void *obj){
    rev_map *x = (rev_map *) obj;
    int i, j;
    (*x)(5, &i, &j);
    std::cout << "i: " << i << ", j: " << j << std::endl;
}

int main (){
    int n = 3;
    rev_map x(n);
    foo((void*)&x);
}

Polimorfismo

Além da simplicidade, outra vantagem de passar objetos é fazer uso do polimorfismo, característico de orientação a objetos.

No meu código, eu escrevi uma classe encapsulando a API do Xpress, para facilitar a criação de modelos PLI, a qual chamei de Xpress. Ao criar um modelo novo, eu geralmente defino uma nova classe herdando de Xpress. Considere Model1 e Model2 dois tais modelos.

struct Xpress {

};
struct Model1 : Xpress {

};
struct Model2 : Xpress {

};

Cada modelo tem seu conjunto de variáveis, mas eu gostaria que a callback imprimisse o nome das variáveis de maneira transparente. A minha solução foi definir rev_map como classes internas a esses modelos, inclusive na classe Xpress, pois é essa classe que vai garantir a transparência.

struct Xpress {
    struct rev_map {
    };
};
struct Model1 : Xpress {    
    struct rev_map : Xpress::rev_map {
    };
};
struct Model2 : Xpress {
    struct rev_map : Xpress::rev_map {
    };
};

A vantagem de usar classes internas é que elas podem ter o mesmo nome. A desambiguação é sempre feita de acordo com o escopo. Para selecionar rev_map de uma classe específica, basta qualificá-lo como por exemplo Xpress::rev_map.

E mais, como iremos chamar o operator (), recebendo col, *i, *j, para todo objeto rev_map, devemos forçar que todas as classes derivadas de Xpress::rev_map implementem-no. Podemos fazer com que rev_map em Xpress se comporte como uma interface, tornando-a puramente virtual:

struct Xpress {
    struct rev_map {
        virtual void operator() 
            (int col, int *i, int *j) = 0; 
    };
};

O restante do modelo fica:

struct Model1 : Xpress {
    
    struct rev_map : Xpress::rev_map {
        void operator() (int col, int *i, int *j) {
            printf("model1\n");
        } 
    };
};
struct Model2 : Xpress {
    struct rev_map : Xpress::rev_map {
        void operator() (int col, int *i, int *j) {
            printf("model2\n");
        } 
    };
};

void foo(void *obj){

    Xpress::rev_map *x = (Xpress::rev_map *) obj;
    int i, j;
    (*x)(5, &i, &j);
}

Para verificar o polimorfismo em ação:

int main (){

    Model1::rev_map a;
    foo((void*)&a); // imprime model1
    Model2::rev_map b;
    foo((void*)&b); // imprime model2
}

Veja que a callback (foo) vai invocar corretamente as classes internas de Model1 e Model2, mesmo que em sua implementação seja usada a classe interna de Xpress.

Os comentários estão fechados.

%d bloggers like this: