c++ – Factorize a function where inheritance is unwanted

I have this piece of code which I’d like to improve:

std::optional<IntersectionInfo> Scene::intersects(const Ray& ray) const
{
    float closest = std::numeric_limits<float>::max();
    int index = -1;

    int i = 0;
    for(auto& sphere : _spheres)
    {
        auto (b, d) = Intersections::intersects(ray, sphere._shape);
        if(b && d < closest)
        {
            closest = d;
            index = i;
        }

        i++;
    }

    i = 0;
    bool isPlane = false;
    for(auto& plane : _planes)
    {
        auto (b, d) = Intersections::intersects(ray, plane._shape);
        if(b && d < closest)
        {
            closest = d;
            index = i;
            isPlane = true;
        }

        i++;
    }

    i = 0;
    bool isBox = false;
    for(auto& box : _boxes)
    {
        auto (b, d) = Intersections::intersects(ray, box._shape);
        if(b && d < closest)
        {
            closest = d;
            index = i;
            isBox = true;
        }
    }

    i = 0;
    bool isTri = false;
    for(auto& tri : _triangles)
    {
        auto (b, d) = Intersections::intersects(ray, tri._shape);
        if(b && d < closest)
        {
            closest = d;
            index = i;
            isTri = true;
        }
    }

    if(index != -1)
    {
        IntersectionInfo info;
        info._intersection = ray._position + ray._direction * closest;

        if(isTri)
        {
            info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, _triangles(index)._shape);
            info._material = *_triangles(index)._material;
        }
        else if(isBox)
        {
            info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, _boxes(index)._shape);
            info._material = *_boxes(index)._material;
        }
        else if(isPlane)
        {
            info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, _planes(index)._shape);
            info._material = *_planes(index)._material;
        }
        else
        {
            info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, _spheres(index)._shape);
            info._material = *_spheres(index)._material;
        }

        info._intersection += info._normal * 0.001f;

        return info;
    }

    return {};
}

This function operates over several vectors (_spheres, _planes, _boxes and _triangles) which stores different types. Since the code is syntactically identical (but intersects and computeIntersectionNormal calls varies depending on the input type), I’d like to find a way to improve it.

An obvious solution would be to use inheritance and have a single vector storing a Shape, which would have virtual members for intersects and computeInteresctionNormal, however :

  • I do not wish to change the existing type structures just for the sake of this function.
  • This function is an hot loop of my program inheritance has shown a visible cost.

I’d also would like to avoid macros (unless they are really simple).

I came up with this :

enum class ShapeType
{
    None,
    Sphere,
    Plane,
    Box,
    Triangle
};

template<typename Shape>
std::function<IntersectionInfo()> intersectsWithShapes(const std::vector<MaterialShape<Shape>>& materialShapes, const Ray& ray, ShapeType currentType, float& closest, int& index, ShapeType& type)
{
    int i = 0;
    for(const auto& materialShape : materialShapes)
    {
        auto (b, d) = Intersections::intersects(ray, materialShape._shape);
        if(b && d < closest)
        {
            closest = d;
            index = i;
            type = currentType;
        }

        i++;
    }

    return (&)()
    {
        IntersectionInfo info;
        info._intersection = ray._position + ray._direction * closest;
        info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, materialShapes(index)._shape);
        info._material = *materialShapes(index)._material;

        info._intersection += info._normal * 0.001f;

        return info;
    };
}

std::optional<IntersectionInfo> Scene::intersects(const Ray& ray) const
{
    float closest = std::numeric_limits<float>::max();
    int index = -1;
    auto type = ShapeType::None;

    auto F1 = intersectsWithShapes(_spheres, ray, ShapeType::Sphere, closest, index, type);
    auto F2 = intersectsWithShapes(_planes, ray, ShapeType::Plane, closest, index, type);
    auto F3 = intersectsWithShapes(_boxes, ray, ShapeType::Box, closest, index, type);
    auto F4 = intersectsWithShapes(_triangles, ray, ShapeType::Triangle, closest, index, type);

    decltype(F1) F;

    switch(type)
    {
        case ShapeType::None: return {};
        case ShapeType::Sphere: F = F1; break;
        case ShapeType::Plane: F = F2; break;
        case ShapeType::Box: F = F3; break;
        case ShapeType::Triangle: F = F4; break;
    }

    return F();
}

I prefer this over the above function because adding a shape is simpler and less prone to error, and the entire interesting code is located in a small function. But it’s not ideal because now Scene::intersects() is entirely made of boilerplate code, it’s not obvious to guess why intersectsWithShapes returns a lambda, and this introdoces a visible code (althrough this time, only in debug build).