【问题标题】:C++11 indexing template parameter packs at runtime in order to access Nth type运行时 C++11 索引模板参数包以便访问第 N 种类型
【发布时间】:2023-04-09 04:46:01
【问题描述】:

通过这个SO topic(和这个blog post),我知道如何访问模板参数包中的第N 个类型。例如,one of the answers 对上述 SO 问题的建议如下:

template<int N, typename... Ts> using NthTypeOf = typename std::tuple_element<N, std::tuple<Ts...>>::type;

using ThirdType = NthTypeOf<2, Ts...>;

但是,这些方法仅在编译时有效。尝试做一些事情,例如:

int argumentNumber = 2;
using ItsType = NthTypeOf<argumentNumber, Arguments...>;

会导致编译错误:

错误:非类型模板参数不是常量表达式

有没有办法在运行时访问第 N 个类型?


这是我的用例:

我的程序读取一个文本文件,它基本上是一个数字数组。每个数字 i 指的是模板参数包的第 i 种类型,我的类基于该模板参数包。基于该类型,我想声明一个该类型的变量并用它做一些不同的事情。例如,如果它是一个字符串,我想声明一个字符串并进行字符串匹配,如果它是一个整数,我想计算一个数字的平方根。

【问题讨论】:

  • 没有。参数包是编译时构造。
  • 您能否更具体地了解用例。你不能像@NathanOliver 指出的那样那样做,但可能是我们错过了一些你需要实现的信息,并且可以帮助你做一些不同的事情。
  • @narengi:为了实现基于运行时输入的不同行为,解决方案通常涉及多态性(即基类和虚函数),而不是模板元编程。
  • @narengi,请编辑您的问题,以包括您在评论中所说的关于阅读文本文件并根据数字执行不同操作的内容。您应该更详细地了解您要完成的工作。如果它是机密的,那就编造一些类似于你想要完成的事情。
  • 与我自己的问题here有关。答案是 Yakk 的magic switch

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


【解决方案1】:

C++ 是一种静态​类型的语言。因此,所有变量的类型都需要在编译时知道(并且不能改变)。您需要一个依赖于运行时值的类型。幸运的是,C++ 还具有对象动态类型。

警告:此答案中的所有代码仅用于演示基本概念/想法。它缺少任何类型的错误处理、理智的接口(构造函数……)、异常安全……。所以不要用于生产,考虑使用 boost 提供的实现。

要使用此功能,您需要所谓的多态基类:一个具有(至少)一个virtual 成员函数的类,您可以从中派生更多类。

struct value_base {
  // you want to be able to make copies
  virtual std::unique_ptr<value_base> copy_me() const = 0;
  virtual ~value_base () {}
};

template<typename Value_Type>
struct value_of : value_base {
  Value_Type value;

  std::unique_ptr<value_base> copy_me() const {
    return new value_of {value};
  }
};

然后,您可以拥有一个静态类型的指针或对该基类的引用的变量,该变量可以指向/引用来自基类以及任何派生类的对象。如果您有明确定义的接口,则将其编码为虚拟成员函数(想想Shapearea ()name ()、...函数)并通过该基类指针进行调用/参考(as shown in the other answer)。否则使用(隐藏的)动态强制转换来获得具有动态类型的静态类型的指针/引用:

struct any {
  std:: unique_ptr<value_base> value_container;

  // Add constructor

  any(any const & a)
    : value_container (a.value_container->copy_me ())
  {}
  // Move constructor

  template<typename T>
  T & get() {
    value_of<T> * typed_container
        = dynamic_cast<value_of<T> *>(value_container.get();)
    if (typed_container == nullptr) {
      // Stores another type, handle failure
    }
    return typed_container->value;
  }

  // T const & get() const;
  // with same content as above
};

template<typename T, typename... Args>
any make_any (Args... && args) {
  // Raw new, not good, add proper exception handling like make_unique (C++14?)
  return {new T(std:: forward<Args>(args)...)};
}

由于对象构造是在运行时完成的,因此指向/引用对象的实际类型可能取决于运行时值:

template<typename T>
any read_and_construct (std:: istream & in) {
  T value;
  // Add error handling please
  in >> value;
  return make_any<T>(std:: move (value));
}

// ...

// missing: way of error handling
std::map<int, std:: function<any(std:: istream &)>> construction_map;
construction_map.insert(std::make_pair(1, read_and_construct<double>));
// and more
int integer_encoded_type;
// error handling please
cin >> integer_encoded_type;
// error handling please
any value = construction_map [integer_encoded_type] (cin);

您可能已经注意到,上面的代码也使用了一个明确定义的构造接口。 如果您不打算对返回的 any 对象做很多不同的事情,可能会在程序运行的大部分时间将它们存储在各种数据结构中,那么 使用 any 类型很可能是矫枉过正,您也应该将依赖于类型的代码放入这些构造函数中。

这种any 类的一个严重缺点是它的通用性:可以在其中存储几乎任何类型。这意味着(实际)存储对象的(最大)大小在编译期间是未知的,因此无法使用具有自动持续时间(“堆栈”)的存储(在标准 C++ 中)。这可能会导致使用动态内存(“堆”)的代价高昂,这比自动内存相当慢。每当必须制作许多 any 对象的副本时,此问题就会出现,但如果您只是保留它们的集合,则可能无关紧要(缓存位置除外)。

因此,如果您在编译时知道必须能够存储的类型集,那么您可以(在编译时)计算所需的最大大小,使用它的静态数组大小并在该数组中构造对象(从 C++11 开始,您也可以使用(递归模板)union 实现相同的目的):

constexpr size_t max_two (size_t a, size_t b) {
  return (a > b) ? a : b;
}

template<size_t size, size_t... sizes>
constexpr size_t max_of() {
  return max_two (size, max_of<sizes>());
}

template<typename... Types>
struct variant {
  alignas(value_of<Types>...) char buffer[max_of<sizeof (value_of<Types>)...>()];
  value_base * active;

  // Construct an empty variant
  variant () : active (nullptr)
  {}

  // Copy and move constructor still missing!

  ~variant() {
    if (active) {
      active->~value_base ();
    }
  }

  template<typename T, typename... Args>
  void emplace (Args... && args) {
    if (active) {
      active->~value_base ();
    }
    active = new (buffer) T(std:: forward<Args>(args)...);
  }
};

【讨论】:

    【解决方案2】:

    C++ 是一种静态类型语言,这意味着变量的类型不能在运行时决定或改变。

    由于您的数字数组是在运行时输入的,因此您无法按照您描述的方式使用NthTypeOf 元函数,因为NthTypeOf 只能依赖于编译时索引。

    在您的用例中,不仅变量类型不同,而且行为也会根据用户输入而有所不同。

    如果您想要基于运行时确定的值的不同行为,我建议使用 switch 语句、std::function 的容器或多态“命令”对象的异构容器。

    基于 switch 语句的解决方案非常简单,所以我不会费心展示示例。

    std::function 是一个围绕类函数对象的多态包装器。您可以使用std::function 的容器来构建一种调度表。

    struct StringMatch
    {
        void operator()() const
        {
            std::string s1, s2;
            std::cin >> s1 >> s2;
            if (s1 == s2)
                std::cout << "Strings match\n";
            else
                std::cout << "Strings don't match\n";
        }
    };
    
    struct SquareRoot
    {
        void operator()() const
        {
            float x = 0;
            std::cin >> x;
            std::cout << "Square root is " << std::sqrt(x) <<"\n";
        }
    
    };
    
    int main()
    {
        const std::map<int, std::function> commands =
        {
            {1, StringMatch()},
            {2, SquareRoot()},
        };
    
        int commandId = 0;
        std::cin >> commandId;
    
        auto found = command.find(commandId);
        if (found != commands.end())
            (*found->second)();
        else
            std::cout << "Unknown command";
    
        return 0;
    }
    

    map 当然可以替换为平面数组或向量,但是您需要担心命令 ID 范围内的“漏洞”。


    如果您需要命令对象能够执行更多操作而不是执行自身(例如具有属性,或支持撤消/重做),您可以使用使用多态性并受传统 Command Pattern 启发的解决方案。

    class Command
    {
    public:
        virtual ~Command() {}
        virtual void execute();
        virtual std::string name() const;
        virtual std::string description() const;
    };
    
    class StringMatch : public Command
    {
    public:
        void execute() override
        {
            std::string s1, s2;
            std::cin >> s1 >> s2;
            if (s1 == s2)
                std::cout << "Strings match\n";
            else
                std::cout << "Strings don't match\n";
        }
    
        std::string name() const override {return "StringMatch";}
        std::string description() const override {return "Matches strings";}
    };
    
    class SquareRoot : public Command
    {
    public:
        void execute() override
        {
            float x = 0;
            std::cin >> x;
            std::cout << "Square root is " << std::sqrt(x) <<"\n";
        }
    
        std::string name() const override {return "SquareRoot";}
        std::string description() const override {return "Computes square root";}
    };
    
    int main()
    {
        constexpr int helpCommandId = 0;
    
        const std::map<int, std::shared_ptr<Command>> commands =
        {
            {1, std::make_shared<StringMatch>()},
            {2, std::make_shared<SquareRoot>()},
        };
    
        int commandId = 0;
        std::cin >> commandId;
    
        if (commandId == helpCommandId)
        {
            // Display command properties
            for (const auto& kv : commands)
            {
                int id = kv.first;
                const Command& cmd = *kv.second;
                std::cout << id << ") " << cmd.name() << ": " << cmd.description()
                          << "\n";
            }
        }
        else
        {
            auto found = command.find(commandId);
            if (found != commands.end())
                found->second->execute();
            else
                std::cout << "Unknown command";
        }
    
        return 0;
    }
    

    尽管 C++ 是一种静态类型语言,但仍有一些方法可以模拟 Javascript 样式的动态变量,例如 JSON for Modern C++ libraryBoost.Variant

    Boost.Any 也可用于命令参数的类型擦除,并且您的命令对象/函数会知道如何将它们向下转换回它们的静态类型。

    但此类模拟的动态变量无法满足您根据用户/文件输入具有不同行为的需求。

    【讨论】:

    • 我投了反对票,因为你回答的第一句话是错误的:strong typing 意味着类型决定了人们可以对值做什么(就像你不能只调用 foo(bar) baz)。你的意思是静态类型。不过,其余的答案都很好!
    • 哦,你错过了一个虚拟析构函数(虽然在这里不是绝对必要的,但我会说这是一种很好的做法)
    • @DanielJour 感谢您解释您的反对意见。我将修改“强类型”部分,我想我需要详细了解这在编程语言中的确切含义。
    • 对于像我这样需要强烈/每周与静态/动态类型语言的提醒的人:stackoverflow.com/questions/2351190/…
    • 在 2017 年,我们真的不应该推荐或链接“命令”模式,甚至不应该用那个名字来提及它。它只是通过 90 年代设计模式解决方案来解决 90 年代时代 OO 语言中的巨大缺陷。老手不可避免地知道这一点,但没有理由将语言新手介绍给包袱。我们应该只讨论传递函数和std::function/lambdas/函数指针。
    【解决方案3】:

    当您想在本地使用依赖于运行时的类型做某事时,一种可能的方法是在编译时预测运行时值。

    using Tuple = std::tuple<int, double, char>;
    
    int type;
    std::cin >> type;
    switch(type) {
        case 0: {
                    using ItsType = std::tuple_element<0, Tuple>;
                    break;
                }
        case 1: {
                    using ItsType = std::tuple_element<1, Tuple>;
                    break;
                }
        default: std::cerr << "char is not handled yet." << std::endl;
                 break;
    }
    

    当然,只适用于小型包装。

    【讨论】:

      【解决方案4】:

      有没有办法在运行时访问第 N 种类型?

      是的,尽管根据其他答案,在这种情况下可能不合适。

      适配这个answer,可以在编译时迭代,选择类型。

      #include <iostream>
      #include <fstream>
      #include <string>
      #include <type_traits>
      #include <tuple>
      #include <cmath>
      
      std::ifstream in("my.txt");
      
      void do_something(const std::string& x)
      {
          std::cout << "Match " << x << '\n';
      }
      
      void do_something(int x)
      {
          std::cout << "Sqrt of " << x << " = " << std::sqrt(x) << '\n';
      }
      
      template<std::size_t I, typename... Tp>
      inline typename std::enable_if_t<I == sizeof...(Tp)> action_on_index_impl(size_t)
      { // reached end with I==number of types: do nothing
      }
      template<std::size_t I, typename... Tp>
      inline typename std::enable_if_t<I < sizeof...(Tp)> action_on_index_impl(size_t i)
      {
          if (i == I){
              // thanks to https://stackoverflow.com/a/29729001/834521 for following
              std::tuple_element_t<I, std::tuple<Tp...>> x{};
              in >> x;
              do_something(x);
          }
          else
              action_on_index_impl<I+1, Tp...>(i);
      }
      template<typename... Tp> void action_on_index(size_t i)
      {
          // start at the beginning with I=0
          action_on_index_impl<0, Tp...>(i);
      }
      
      int main()
      {
          int i{};
          while(in >> i, in)
              action_on_index<std::string, int>(i);
          return 0;
      }
      

      用我的.txt

      0 hello
      1 9
      0 world
      1 4
      

      输出

      Match hello
      Sqrt of 9 = 3
      Match world
      Sqrt of 4 = 2
      

      我需要知道如何在运行时在不同的上下文中访问第 N 种类型,因此我在这里回答(我想知道是否有更好的方法,尤其是在 C++14/17 中)。

      【讨论】:

        猜你喜欢
        • 2013-12-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-07-27
        • 1970-01-01
        • 2012-07-10
        • 1970-01-01
        相关资源
        最近更新 更多