Eu estava tentando projetar uma pequena API geométrica C ++ para fins de aprendizado, mas me deparei com um problema ao lidar com interseções de entidades geométricas. Por exemplo, a interseção de uma linha e uma esfera pode ter três tipos diferentes: um par de pontos, um ponto ou nada. Eu encontrei várias maneiras de lidar com esse problema, mas não sei qual delas parece ser a melhor:
CGAL Object
tipo de retorno
Primeiro tentei ver o que foi feito em outras bibliotecas geométricas. O mais completo que encontrei foi o CGAL. No CGAL, as funções de interseção retornam um Object
, que é um tipo genérico que pode conter qualquer coisa (como boost::any
). Em seguida, você tenta atribuir o Object
a um valor de outro tipo, aqui está um exemplo:
void foo(CGAL::Segment_2<Kernel> seg, CGAL::Line_2<Kernel> line)
{
CGAL::Object result;
CGAL::Point_2<Kernel> ipoint;
CGAL::Segment_2<Kernel> iseg;
result = CGAL::intersection(seg, line);
if (CGAL::assign(ipoint, result)) {
// handle the point intersection case.
} else if (CGAL::assign(iseg, result)) {
// handle the segment intersection case.
} else {
// handle the no intersection case.
}
}
A propósito, a função assign
usa dynamic_cast
para verificar se as duas variáveis são designáveis (todo mundo adora o RTTI).
Tipo de retorno da união
Uma união também pode ser usada como um tipo de retorno, mas significa usar um id
para cada tipo geométrico e ter algum tipo de tipo de variante para cobrir todos os problemas.
struct Variant
{
int id;
union
{
Point point;
std::pair<Point, Point> pair;
};
};
int main()
{
Point point;
std::pair<Point, Point> pair;
Variant v = intersection(Line{}, Sphere{});
if (v.id == ID_POINT)
{
point = v.point;
// Do something with point
}
else if (v.id == ID_VARIANT)
{
pair = v.pair;
// Do something with pair
}
else
{
// No intersection
}
}
Evitando o problema "sem interseção"
Para dividir o caso "sem interseção" do problema principal, algumas bibliotecas exigiam que o usuário verificasse se havia uma interseção antes de tentar descobrir qual era o tipo de retorno da interseção. Ficou assim:
Line line;
Sphere sphere;
if (intersects(line, sphere))
{
auto ret = intersection(line, sphere);
// Do something with ret
}
Outra maneira de dividir o caso "sem interseção" do resto do problema seria usar um tipo optional
:
std::optional<...> ret = intersection(Line{}, Sphere{});
if (ret)
{
// Do something with ret
}
Usando exceções para controlar o tipo "retorno"
Eu já posso ver alguns de vocês chorando.
No entanto, outra maneira de lidar com esse problema de ter diferentes tipos de retorno seria throw
dos resultados, em vez de retorná-los. O retorno "sem interseção" ainda pode usar uma das técnicas do parágrafo anterior:
try
{
intersection(Line{}, Sphere{});
}
catch (const Point& point)
{
// Do something with point
}
catch (const std::pair<Point, Point>& pair)
{
// Do something with pair
}
// Can still catch errors (or "no intersection" special type?)
Ter um tipo de retorno "principal", jogando os outros
Outra forma seria ter um tipo de retorno "principal" para a interseção, digamos std::pair<Point, Point>
e considerando os outros tipos de retorno como "excepcionais"; Isso dá mais significado ao uso de exceções, enquanto isso ainda não é uma boa gestão de erros. Por outro lado, pode parecer estranho lidar com um tipo "main" diferente dos outros ...
try
{
auto pair = intersection(Line{}, Sphere{});
// Do something with pair
}
catch (const Point& point)
{
// The chances for a sphere and a line to meet at
// a single point are so small that the case can
// already be considered exceptional.
// ...
}
Eu propositalmente deixei a gestão de erros e o caso "sem interseção" fora deste último exemplo, já que muitas técnicas já descritas poderiam ser usadas para lidar com isso e eu não quero que o número de exemplos seja exponencial. Aqui está um deles:
try
{
// res is optional<pair<Point, Point>>
if (auto res = intersection(Line{}, Sphere{}))
{
std::cout << "intersection: two points" << '\n'
<< std::get<0>(*res).x() << " "
<< std::get<0>(*res).y() << '\n'
<< std::get<1>(*res).x() << " "
<< std::get<1>(*res).y() << '\n';
}
else
{
std::cout << "no intersection" << '\n';
}
}
catch (const Point& point)
{
// Exceptional case
std::cout << "intersection: one point" << '\n'
<< point.x() << " "
<< point.y() << '\n';
}
Como os casos em que o resultado é lançado são excepcionais, ele não deve adicionar nenhuma sobrecarga de tempo de execução ao programa se o sistema subjacente usar exceções de custo zero em vez do antigo sistema de exceções SJLJ.
A pergunta atual
Bem, essa foi uma introdução bem longa, então aqui está a pergunta: existe uma solução idiomática para o problema nesses exemplos? E se não, há pelo menos alguns dos exemplos que poderiam ser banidos sem um segundo pensamento (bem, você poderia dar um segundo pensamento para os exemplos de exceção embora ...)?
Nota: algo parecia evidente para mim, mas aparentemente não é: a entidade retornada pela interseção entre duas entidades geométricas não está limitada a um nada, um ponto ou um conjunto de pontos. Uma interseção pode praticamente retornar qualquer entidade geométrica. Portanto, usar um contêiner para manter os valores não parece uma boa ideia.
Nota 2: nota da máquina do tempo, acontece. Depois de ter pensado nisso, eu baniria exceções (como a maioria das pessoas faria), não porque as exceções são inerentemente ruins ou não projetadas para isso, mas porque faz com que a expressão intersection(...) == SomeShape
lance uma exceção mesmo quando os dois operandos deveriam ser igual, o que não é elegante. Eu acho que eu usaria um filho bastardo de uma variante.