#include <array>
#include <cstddef>
#include <tuple>
#include <type_traits>
#include <iostream>

// implementation

namespace detail
{
    template<std::size_t I>
    struct visit_impl
    {
        template<typename Tuple, typename F, typename ...Args>
        inline static constexpr int visit(Tuple const &tuple, std::size_t idx, F fun, Args &&...args) noexcept(noexcept(fun(std::get<I - 1U>(tuple), std::forward<Args>(args)...)) && noexcept(visit_impl<I - 1U>::visit(tuple, idx, fun, std::forward<Args>(args)...)))
        {
            return (idx == (I - 1U) ? (fun(std::get<I - 1U>(tuple), std::forward<Args>(args)...), void(), 0) : visit_impl<I - 1U>::visit(tuple, idx, fun, std::forward<Args>(args)...));
        }

        template<typename R, typename Tuple, typename F, typename ...Args>
        inline static constexpr R visit(Tuple const &tuple, std::size_t idx, F fun, Args &&...args) noexcept(noexcept(fun(std::get<I - 1U>(tuple), std::forward<Args>(args)...)) && noexcept(visit_impl<I - 1U>::template visit<R>(tuple, idx, fun, std::forward<Args>(args)...)))
        {
            return (idx == (I - 1U) ? fun(std::get<I - 1U>(tuple), std::forward<Args>(args)...) : visit_impl<I - 1U>::template visit<R>(tuple, idx, fun, std::forward<Args>(args)...));
        }
    };

    template<>
    struct visit_impl<0U>
    {
        // template parameter 'I' reached 0U, which means the runtime index did not exist in the tuple (we could throw an exception, but we'll ignore it here)
        template<typename Tuple, typename F, typename ...Args>
        inline static constexpr int visit(Tuple const&, std::size_t, F, Args&&...) noexcept
        {
            return 0;
        }

        template<typename R, typename Tuple, typename F, typename ...Args>
        inline static constexpr R visit(Tuple const&, std::size_t, F, Args&&...) noexcept(noexcept(R{}))
        {
            static_assert(std::is_default_constructible<R>::value, "Explicit return type of visit_at method must be default-constructible");
            return R{};
        }
    };
}

template<typename Tuple, typename F, typename ...Args>
inline constexpr void visit_at(Tuple const &tuple, std::size_t idx, F fun, Args &&...args) noexcept(noexcept(detail::visit_impl<std::tuple_size<Tuple>::value>::visit(tuple, idx, fun, std::forward<Args>(args)...)))
{
    detail::visit_impl<std::tuple_size<Tuple>::value>::visit(tuple, idx, fun, std::forward<Args>(args)...);
}

template<typename R, typename Tuple, typename F, typename ...Args>
inline constexpr R visit_at(Tuple const &tuple, std::size_t idx, F fun, Args &&...args) noexcept(noexcept(detail::visit_impl<std::tuple_size<Tuple>::value>::template visit<R>(tuple, idx, fun, std::forward<Args>(args)...)))
{
    return detail::visit_impl<std::tuple_size<Tuple>::value>::template visit<R>(tuple, idx, fun, std::forward<Args>(args)...);
}

// demo code

int main(int, char**) noexcept
{
	static constexpr auto const visitor = [](auto &&v) // standard visitor
	{
		std::cout << v << std::endl;
	};
	static constexpr auto const visitor_with_result = [](auto &&v, int inc = 1) -> int // visitor that returns a result (optionally with second parameter)
	{
		return v + inc;
	};
	static constexpr auto const tup = std::make_tuple(1, 2, 3, 4, 5);
	static constexpr std::array<int, 5U> const arr{ 6, 7, 8, 9, 10 };
	for (int i = 0; i < 5; ++i)
	{
		visit_at(tup, i, visitor); // call standard visitor on tuple
	}
	std::cout << std::endl;
	for (int i = 0; i < 5; ++i)
	{
		std::cout << visit_at<int>(tup, i, visitor_with_result) << std::endl; // call visitor with result using default second parameter
	}
	std::cout << std::endl;
	for (int i = 0; i < 5; ++i)
	{
		std::cout << visit_at<int>(arr, i, visitor_with_result, 10) << std::endl; // call visitor with result using explicit second parameter and array instead of tuple
	}
	return 0;
}
