【问题标题】:Cannot remove unwanted overloads无法删除不需要的重载
【发布时间】:2015-09-28 23:24:49
【问题描述】:

transform 执行的函数

const std::vector<int>         a = {1,     2,       3,       4,    5};
const std::vector<double>      b = {1.2,   4.5,     0.6};
const std::vector<std::string> c = {"hi", "howdy", "hello", "bye"};
std::vector<double> result(5);

transform<Foo> (result.begin(),
    a.begin(), a.end(),
    b.begin(), b.end(),
    c.begin(), c.end());

是在多个容器上对std::transform进行泛化,将结果输出到向量result中。显然需要一个带有签名(int, double, const std::string&amp;) 的函数来处理此示例中的三个容器。但是因为容器有不同的长度,我们需要使用一些重载。我将使用持有者类 Foo 的这些成员重载来测试它:

static int execute (int i, double d, const std::string& s) {return i + d + s.length();}
static int execute (int i, const std::string& s) {return 2 * i + s.length();}
static int execute (int i) {return 3 * i - 1;}

但是,除非我定义其他三个从未调用过的重载,即带有参数(int, double)(const std::string&amp;)(),否则程序将无法编译。我想删除这些重载,但程序不会让我这样做。你可以想象如果我们有超过 3 个容器(不同长度的)这会导致的问题,当它们甚至不被使用时,强制定义具有许多参数排列的重载。

这是我的工作程序,它显然会说明为什么需要这些无关的重载。我不明白如何或为什么要强制定义它们,并且想删除它们。为什么它们必须在那里,以及如何消除对它们的需求?

#include <iostream>
#include <utility>
#include <tuple>

bool allTrue (bool a) {return a;}

template <typename... B>
bool allTrue (bool a, B... b) {return a && allTrue(b...);}

template <typename F, size_t... Js, typename Tuple>
typename F::return_type screenArguments (std::index_sequence<>, std::index_sequence<Js...>, Tuple& tuple) {
    return F::execute (*std::get<Js>(tuple)++...);
}

// Thanks to Barry for coming up with screenArguments.
template <typename F, std::size_t I, size_t... Is, size_t... Js, typename Tuple>
typename F::return_type screenArguments (std::index_sequence<I, Is...>, std::index_sequence<Js...>, Tuple& tuple) {
    if (std::get<2*I>(tuple) != std::get<2*I+1>(tuple))
        return screenArguments<F> (std::index_sequence<Is...>{}, std::index_sequence<Js..., 2*I>{}, tuple);
    else
        return screenArguments<F> (std::index_sequence<Is...>{}, std::index_sequence<Js...>{}, tuple);
}

template <typename F, typename Tuple>
typename F::return_type passCertainArguments (Tuple& tuple) {
    return screenArguments<F> (std::make_index_sequence<std::tuple_size<Tuple>::value / 2>{},
        std::index_sequence<>{}, tuple);
}

template <typename F, typename OutputIterator, std::size_t... Is, typename... InputIterators>
OutputIterator transformHelper (OutputIterator result, const std::index_sequence<Is...>&, InputIterators... iterators) {
    auto tuple = std::make_tuple(iterators...);
    while (!allTrue(std::get<2*Is>(tuple) == std::get<2*Is + 1>(tuple)...))
        *result++ = passCertainArguments<F>(tuple);
    return result;
}

template <typename F, typename OutputIterator, typename... InputIterators>
OutputIterator transform (OutputIterator result, InputIterators... iterators) {
    return transformHelper<F> (result, std::make_index_sequence<sizeof...(InputIterators) / 2>{}, iterators...);
}

// Testing
#include <vector>

struct Foo {
    using return_type = int;
    static int execute (int i, double d, const std::string& s) {return i + d + s.length();}
    static int execute (int i, const std::string& s) {return 2 * i + s.length();}
    static int execute (int i) {return 3 * i - 1;}
    // These overloads are never called, but apparently must still be defined.  
    static int execute () {std::cout << "Oveload4 called.\n";  return 0;}
    static int execute (int i, double d) {std::cout << "Oveload5 called.\n";  return i + d;}
    static int execute (const std::string& s) {std::cout << "Oveload6 called.\n";  return s.length();}
};

int main() {
    const std::vector<int>         a = {1,     2,       3,       4,    5};
    const std::vector<double>      b = {1.2,   4.5,     0.6};
    const std::vector<std::string> c = {"hi", "howdy", "hello", "bye"};
    std::vector<double> result(5);

    transform<Foo> (result.begin(),
        a.begin(), a.end(),
        b.begin(), b.end(),
        c.begin(), c.end());
    for (double x : result) std::cout << x << ' ';  std::cout << '\n';
    // 4 11 8 11 14 (correct output)
}

【问题讨论】:

  • 不要乱用模板。如果两个容器的类型相同,你会怎么做?
  • 因为如果字符串序列比其他两个更长,则需要这些重载。由于序列的长度是运行时属性,因此您无法在编译时知道它们是否需要。 (编译器不需要完整组合的唯一原因是double 可以隐式转换为int。)
  • 我怀疑是这样的。那么避免这个问题的正确设计是什么?如果这样的解决方案过于艰巨,您至少可以提出另一种方法吗?
  • 你能解释一下为什么你首先需要这个吗?整个省略论点的事情似乎很脆弱和​​可疑。
  • 抱歉,只是在练习一些模板编程。随意忽略这个问题,除非你发现它足够有趣来解决它(正确地)。因此,您建议忘记使用重载,并尝试使用多个具有相同签名(int, double, const std::string&amp;) 的 lambda,然后在运行时根据缺少哪些参数调用适当的 lambda?我首先想到了这个,但很快就放弃了这个想法。

标签: c++ templates c++11 variadic


【解决方案1】:

编译器在编译时不知道在运行时将使用哪些函数组合。所以你必须为每个组合实现所有2^N 函数。此外,当您拥有相同类型的容器时,您的方法也不起作用。

如果你想坚持使用模板,我的想法是实现这样的功能:

template <bool Arg1, bool Arg2, bool Arg3>
static int execute (int *i, double *d, const std::string *s);

模板参数Arg1, Arg2, Arg3 代表每个参数的有效性。编译器将为每个参数组合自动生成所有2^N 实现。随意在此函数中使用 if 语句而不是模板特化 - 它们将在编译时解析为 if (true)if (false)

【讨论】:

  • 我找到了一个新的解决方案,欢迎您的批评。包括说明它会崩溃的情况。
【解决方案2】:

我想我明白了!根据实际需要的参数数量模板化函数Foo::execute,并让它们都具有相同的参数:

struct Foo {
    using return_type = int;
    template <std::size_t> static return_type execute (int, double, const std::string&);
};

template <> Foo::return_type Foo::execute<3> (int i, double d, const std::string& s) {return i + d + s.length();}
template <> Foo::return_type Foo::execute<2> (int i, double, const std::string& s) {return 2 * i + s.length();}
template <> Foo::return_type Foo::execute<1> (int i, double, const std::string&) {return 3 * i - 1;}
template <> Foo::return_type Foo::execute<0> (int, double, const std::string&) {return 0;}  // The only redundant specialization that needs to be defined.

这是完整的解决方案。

#include <iostream>
#include <utility>
#include <tuple>
#include <iterator>

bool allTrue (bool b) {return b;}

template <typename... Bs>
bool allTrue (bool b, Bs... bs) {return b && allTrue(bs...);}

template <typename F, std::size_t N, typename Tuple, typename... Args>
typename F::return_type countArgumentsNeededAndExecute (Tuple&, const std::index_sequence<>&, Args&&... args) {
    return F::template execute<N>(std::forward<Args>(args)...);
}

template <typename F, std::size_t N, typename Tuple, std::size_t I, size_t... Is, typename... Args>
typename F::return_type countArgumentsNeededAndExecute (Tuple& tuple, const std::index_sequence<I, Is...>&, Args&&... args) {  // Pass tuple by reference, because its iterator elements will be modified (by being incremented).
    return (std::get<2*I>(tuple) != std::get<2*I + 1>(tuple)) ?
        countArgumentsNeededAndExecute<F, N+1> (tuple, std::index_sequence<Is...>{}, std::forward<Args>(args)...,  // The number of arguments to be used increases by 1.
            *std::get<2*I>(tuple)++) :  // Pass the value that will be used and increment the iterator.
        countArgumentsNeededAndExecute<F, N> (tuple, std::index_sequence<Is...>{}, std::forward<Args>(args)...,
            typename std::iterator_traits<typename std::tuple_element<2*I, Tuple>::type>::value_type{});  // Pass the default value (it will be ignored anyway), and don't increment the iterator.  Hence, the number of arguments to be used does not change.
}

template <typename F, typename OutputIterator, std::size_t... Is, typename... InputIterators>
OutputIterator transformHelper (OutputIterator result, const std::index_sequence<Is...>& indices, InputIterators... iterators) {
    auto tuple = std::make_tuple(iterators...);  // Cannot be const, as the iterators are being incremented.
    while (!allTrue(std::get<2*Is>(tuple) == std::get<2*Is + 1>(tuple)...))
        *result++ = countArgumentsNeededAndExecute<F, 0> (tuple, indices);  // Start the count at 0.  Examine 'indices', causing the count to increase one by one.
    return result;
}

template <typename F, typename OutputIterator, typename... InputIterators>
OutputIterator transform (OutputIterator result, InputIterators... iterators) {
    return transformHelper<F> (result, std::make_index_sequence<sizeof...(InputIterators) / 2>{}, iterators...);
}

// Testing
#include <vector>

struct Foo {
    using return_type = int;
    template <std::size_t> static return_type execute (int, double, const std::string&);
};

// Template the function Foo::execute according to the number of arguments that are actually needed:
template <> Foo::return_type Foo::execute<3> (int i, double d, const std::string& s) {return i + d + s.length();}
template <> Foo::return_type Foo::execute<2> (int i, double, const std::string& s) {return 2 * i + s.length();}
template <> Foo::return_type Foo::execute<1> (int i, double, const std::string&) {return 3 * i - 1;}
template <> Foo::return_type Foo::execute<0> (int, double, const std::string&) {return 0;}  // The only redundant specialization that needs to be defined.

int main() {
    const std::vector<int>         a = {1,     2,       3,       4,    5};
    const std::vector<double>      b = {1.2,   4.5,     0.6};
    const std::vector<std::string> c = {"hi", "howdy", "hello", "bye"};
    std::vector<double> result(5);

    transform<Foo> (result.begin(),
        a.begin(), a.end(),
        b.begin(), b.end(),
        c.begin(), c.end());
    for (double x : result) std::cout << x << ' ';  std::cout << '\n';
    // 4 11 8 11 14 (correct output)
}

第二个解决方案是使用 Andrey Nasonov 的 bool 模板生成所需的所有 2^N 重载。请注意,上述解决方案只需要 N+1 个模板实例来实现重载。

#include <iostream>
#include <utility>
#include <tuple>

bool allTrue (bool b) {return b;}

template <typename... Bs>
bool allTrue (bool b, Bs... bs) {return b && allTrue(bs...);}

template <bool...> struct BoolPack {};

template <typename F, typename Tuple, bool... Bs, typename... Args>
typename F::return_type checkArgumentsAndExecute (const Tuple&, const std::index_sequence<>&, BoolPack<Bs...>, Args&&... args) {
    return F::template execute<Bs...>(std::forward<Args>(args)...);
}

template <typename F, typename Tuple, std::size_t I, size_t... Is, bool... Bs, typename... Args>
typename F::return_type checkArgumentsAndExecute (Tuple& tuple, const std::index_sequence<I, Is...>&, BoolPack<Bs...>, Args&&... args) {  // Pass tuple by reference, because its iterators elements will be modified (by being incremented).
    return (std::get<2*I>(tuple) != std::get<2*I+1>(tuple)) ?
        checkArgumentsAndExecute<F> (tuple, std::index_sequence<Is...>{}, BoolPack<Bs..., true>{}, std::forward<Args>(args)...,
            *std::get<2*I>(tuple)++) :  // Pass the value that will be used and increment the iterator.
        checkArgumentsAndExecute<F> (tuple, std::index_sequence<Is...>{}, BoolPack<Bs..., false>{}, std::forward<Args>(args)...,
            typename std::iterator_traits<typename std::tuple_element<2*I, Tuple>::type>::value_type{});  // Pass the default value (it will be ignored anyway), and don't increment the iterator.
}

template <typename F, typename OutputIterator, std::size_t... Is, typename... InputIterators>
OutputIterator transformHelper (OutputIterator& result, const std::index_sequence<Is...>& indices, InputIterators... iterators) {
    auto tuple = std::make_tuple(iterators...);  // Cannot be const, as the iterators are being incremented.
    while (!allTrue(std::get<2*Is>(tuple) == std::get<2*Is + 1>(tuple)...))
        *result++ = checkArgumentsAndExecute<F> (tuple, indices, BoolPack<>{});
    return result;
}

template <typename F, typename OutputIterator, typename... InputIterators>
OutputIterator transform (OutputIterator result, InputIterators... iterators) {
    return transformHelper<F> (result, std::make_index_sequence<sizeof...(InputIterators) / 2>{}, iterators...);
}

// Testing
#include <vector>

struct Foo {
    using return_type = int;
    template <bool B1, bool B2, bool B3> static return_type execute (int, double, const std::string&) {return 0;}  // All necessary overloads defined at once here.
};

// Specializations of Foo::execute<B1,B2,B3>(int, double, const std::string&) that will actually be called by transform<Foo> (it is the client's responsibility to define these overloads based on the containers passed to transform<Foo>).
template <> Foo::return_type Foo::execute<true, true, true> (int i, double d, const std::string& s) {return i + d + s.length();}
template <> Foo::return_type Foo::execute<true, false, true> (int i, double, const std::string& s) {return 2 * i + s.length();}
template <> Foo::return_type Foo::execute<true, false, false> (int i, double, const std::string&) {return 3 * i - 1;}

int main() {
    const std::vector<int>         a = {1,     2,       3,       4,    5};
    const std::vector<double>      b = {1.2,   4.5,     0.6};
    const std::vector<std::string> c = {"hi", "howdy", "hello", "bye"};
    std::vector<double> result(5);

    transform<Foo> (result.begin(),
        a.begin(), a.end(),
        b.begin(), b.end(),
        c.begin(), c.end());
    for (double x : result) std::cout << x << ' ';  std::cout << '\n';
    // 4 11 8 11 14 (correct output)
}

【讨论】:

  • 无法传递默认值时怎么办?完美的解决方案还允许通过跳过默认参数将可变数量的参数传递给execute
  • @Andrey Nasonov 说得好。我假设默认构造函数始终有效。我对您的方法的实现(如上所述)也做出了这个假设。但是需要将一些东西传递给没有被使用的参数。所以你建议将每个参数都转换为指针并默认传递nullptr
  • 我的想法是基于你的想法:ideone.com/Cp22ia - 但这里的问题是将std::string 作为参数传递而不是const std::string &amp;
  • 使用const int&amp;const double&amp;const std::string&amp;const Args&amp;... 解决了这个新问题(经过测试)。所以现在显然解决方案是完美无缺的,除了强制 2^N 模板实例化而不是理想情况下只有 N (如在我的第二个解决方案中,但它具有强制可能不存在的默认构造函数调用的更严重的缺点)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-05
  • 1970-01-01
  • 2020-05-09
  • 2021-06-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多