包装递归可变参数模板类会更改行为。为什么?

时间:2019-04-08 05:37:13

标签: c++ c++11 c++17 variadic-templates template-meta-programming

希望这会引起社区中的一些人的兴趣。希望它不太明显,因为我不确定发生了什么。我创建了具有递归定义的可变参数模板类,主要是作为一种有趣的自我挑战。此类类似于元组,此类创建unordered_maps的unordered_maps,具有任意深度,并且在每一层具有任意键类型。因此,例如,您可以创建nested_map<int, std::string, float, int>,然后用map["fred"][3.4][42] = 35;进行设置。这是代码-不太疯狂。

template<typename T, typename K, typename ... KS> struct nested_map_base : std::unordered_map<K, T>
{
  T &operator[](const K &key)
  {
    // just to verify we get to the bottom of things recursively
    std::cout << "base: key = " << key << std::endl;

    return this->std::unordered_map<K, T>::operator[](key);
  }
};

template<typename T, typename New_K, typename K, typename ... KS>
struct nested_map_base<T, New_K, K, KS ...>
: std::unordered_map<New_K, nested_map_base<T, K, KS...>>
{
  nested_map_base<T, K, KS...> &operator[](const New_K &new_key)
  {
    // just for debugging and to demonstrate that it's working
    // for purposes of this question
    std::cout << "midway: key = " << new_key << std::endl;

    return this->std::unordered_map<New_K, nested_map_base<T, K, KS...>>::operator[](new_key);
  }
};

工作正常。运行以下代码,获得预期的输出-

std::cout << "Method1:" << std::endl << std::endl;
nested_map_base<int, std::string, double, int> test_nest;
std::cout << "insert" << std::endl;
test_nest["leonard"][4.8][45] = 111;
std::cout << "retrieve" << std::endl;
int &answer = test_nest["leonard"][4.8][45];
std::cout << "Aanswer should be 111. Answer is " << answer << std::endl << std::endl;

产生-

Method1:

insert
midway: key = leonard
midway: key = 4.8
base: key = 45
retrieve
midway: key = leonard
midway: key = 4.8
base: key = 45
Aanswer should be 111. Answer is 111

整洁。然后我认为我想将其包装在一个外部类中以保持实现私有,所以我就是这样开始的-

template<typename datum_type, typename ... keys> class nested_map
{
private:
  nested_map_base<datum_type, keys ...> backing_store;

public:
  template<typename Base_key, typename ... KS> auto operator[](const Base_key &key)
  {
    return backing_store[key];
  }
};

那里什么都没有,起初看起来似乎可以,但是下面的代码产生了不同的结果-

std::cout << "Method2:" << std::endl << std::endl;
nested_map<int, std::string, double, int> test_nest;
std::cout << "insert" << std::endl;
test_nest["leonard"][4.8][45] = 111;
std::cout << "retrieve" << std::endl;
int &answer = test_nest["leonard"][4.8][45];
std::cout << "Answer should be 111. Answer is " << answer << std::endl << std::endl;

它产生了这个-

Method2:

insert
midway: key = leonard
midway: key = 4.8
base: key = 45
retrieve
midway: key = leonard
midway: key = 4.8
base: key = 45
Answer should be 111. Answer is 0

递归可变参数模板元编程充满了陷阱,并且有一些原因使事情没有被频繁包装,所以让被包装的那个包装没有用,我并不感到震惊,但是令我惊讶的是它如何没有用它按预期方式递归,直到包含终端基准类型的std::unordered_map。在调试器中,从终端映射中恢复了对int的引用,并在简单测试代码中将其设置为111。您看到密钥第二次被递归的事实表明,检索过程似乎也正在工作,但是引用是零值int。很好奇。

我正在深入调试器中,以查看例如设置引用的实际地址值是否与用于检索的引用相同。我认为它们可能不同的唯一方法是,例如,倒数第二个递归层是否返回最后一层的温度,而不是引用数据结构中的那个。或者,在包装好的情况下,它们都是临时的,而不是引用……之类的东西,但是包装很轻,似乎不可能。因此,如果我发现更多信息,我将添加评论,但是我认为我应该把它扔给社区,以查看是否有一些眼睛可以通过检查挑逗。

1 个答案:

答案 0 :(得分:2)

auto-returning functions的Cppreference页面中的Template Argument Deduction上有一个部分,描述了将auto用作函数的返回值时的规则。

  

auto语句中推断函数return类型的return说明符的含义时,在函数的声明中使用模板自变量推导。

     

对于自动返回函数,按如下方式获取参数P:在T中,声明的包括auto的函数的返回类型,每次出现的auto都将用虚构的类型模板替换参数U。参数A是return语句的表达式,如果return语句没有操作数,则Avoid()。按照上述规则从UP中扣除A之后,将推断出的U代入T中以得到实际的返回类型。

这将解释为什么auto&有效而auto无效的原因。