带有 initializer_list 的编译时查找表

Posted

技术标签:

【中文标题】带有 initializer_list 的编译时查找表【英文标题】:Compile-Time Lookup-Table with initializer_list 【发布时间】:2019-12-03 11:12:44 【问题描述】:

假设您有一些哈希值,并希望在编译时将它们映射到各自的字符串。 理想情况下,我希望能够写出以下内容:

constexpr std::map<int, std::string> map =  1, "1", 2 ,"2" ;

不幸的是,这在 C++17 和 C++2a 中都是不可能的。尽管如此, 我尝试使用std::array 来模拟它,但无法在编译时获取初始化列表的大小,以便在没有明确指定大小的情况下正确设置数组的类型。 这是我的模型:


template<typename T0, typename T1>
struct cxpair

    using first_type = T0;
    using second_type = T1;

    // interestingly, we can't just = default for some reason...
    constexpr cxpair()
        : first(), second()
      

    constexpr cxpair(first_type&& first, second_type&& second)
        : first(first), second(second)
      

    // std::pair doesn't have these as constexpr
    constexpr cxpair& operator=(cxpair<T0, T1>&& other)
     first = other.first; second = other.second; return *this; 

    constexpr cxpair& operator=(const cxpair<T0, T1>& other)
     first = other.first; second = other.second; return *this; 

    T0 first;
    T1 second;
;

template<typename Key, typename Value, std::size_t Size = 2>
struct map

    using key_type = Key;
    using mapped_type = Value;
    using value_type = cxpair<Key, Value>;

    constexpr map(std::initializer_list<value_type> list)
        : map(list.begin(), list.end())
      

    template<typename Itr>
    constexpr map(Itr begin, const Itr &end)
    
        std::size_t size = 0;
        while (begin != end) 
            if (size >= Size) 
                throw std::range_error("Index past end of internal data size");
             else 
                auto& v = data[size++];
                v = std::move(*begin);
            
            ++begin;
        
    

    // ... useful utility methods omitted

private:
    std::array<value_type, Size> data;
    // for the utilities, it makes sense to also have a size member, omitted for brevity
;

现在,如果您只是使用普通的 std::array 来实现,那么开箱即用:


constexpr std::array<cxpair<int, std::string_view>, 2> mapp =  1, "1", 2, "2" ;

// even with plain pair
constexpr std::array<std::pair<int, std::string_view>, 2> mapp =  1, "1", 2, "2" ;

不幸的是,我们必须明确给出数组的大小作为第二个模板参数。这正是我想要避免的。 为此,我尝试构建您在上面看到的地图。 有了这个伙伴,我们可以编写如下内容:

constexpr map<int, std::string_view> mapq =  1, "1" ;
constexpr map<int, std::string_view> mapq =  1, "1", 2, "2" ;

不幸的是,一旦我们超过了 map 中的魔术 Size 常量,我们就会得到一个错误,所以我们需要明确地给出大小:

//// I want this to work without additional shenanigans:
//constexpr map<int, std::string_view> mapq =  1, "1", 2, "2", 3, "3" ;
constexpr map<int, std::string_view, 3> mapq =  1, "1", 2, "2", 3, "3" ;

当然,只要你在 constexpr 范围内throw,你就会得到一个编译错误,并且可以显式地调整魔法常数。但是,这是我想隐藏的实现细节。用户不需要处理这些低级细节,这是编译器应该推断的。

不幸的是,我没有看到具有确切语法 map = ... 的解决方案。我什至看不到constexpr auto map = make_map( ... ); 之类的东西。此外,这是一个与运行时不同的 API,我想避免使用它以增加易用性。

那么,是否有可能在编译时从初始化列表中推断出这个大小参数?

【问题讨论】:

你显示的地图初始化错误,你应该切换整数和字符串。 听起来有点像X-Y problem。您想获得已知值集的快速反向散列吗?你真的需要吗?你的实际问题是什么? 另外,请向我们展示您对std::array的尝试 我已经相应地编辑了我的问题。如果还有什么不清楚的,请告诉我。 @JHBonarius 对字符串字面量很好 【参考方案1】:

std::array有演绎指南:

template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;

让你写:

// ok, a is array<int, 4>
constexpr std::array a = 1, 2, 3, 4;

我们可以按照同样的原则,为map添加一个推演指南like:

template <typename Key, typename Value, std::size_t Size>
struct map 
    constexpr map(std::initializer_list<std::pair<Key const, Value>>)  
;

template <class T, class... U>
map(T, U...) -> map<typename T::first_type, typename T::second_type, sizeof...(U)+1>;

允许:

// ok, m is map<int, int, 3>
constexpr map m = std::pair1, 1, std::pair1, 2, std::pair2, 3;

不幸的是,这种方法需要为初始化列表中的每个类型命名 - 即使在写了 pair1, 1 之后,你也不能只写 1, 2


另一种方法是将右值数组作为参数:

template <typename Key, typename Value, std::size_t Size>
struct map 
    constexpr map(std::pair<Key, Value>(&&)[Size])  
;

这避免了编写推导指南,让您只需在第一个上写类型,代价是额外的一对大括号或括号:

// ok, n is map<int, int, 4>
constexpr map nstd::pair1, 1, 1, 2, 2, 3, 3, 4;

// same
constexpr map n(std::pair1, 1, 1, 2, 2, 3, 3, 4);

请注意,数组是 pair&lt;Key, Value&gt; 而不是 pair&lt;Key const, Value&gt; - 它只允许写入 pair1, 1。由于您无论如何都在编写constexpr 地图,因此这种区别可能并不重要。

【讨论】:

第一个解决方案太麻烦了。不幸的是,第二种解决方案的缺点是用户不能部分专门化map。这可以通过单独的make_map 函数来解决。 @NaCl 这是一个缺点吗?为什么你认为他们不能? @NaCl 另外请不要这样编辑人们的答案。这是一个重大的方向变化。 在某种意义上,因为您可能希望将size_t 存储在地图中,据我所知,地图中没有文字,因此可以推导出为size_t,但也可以做一些不同的事情,即只是unsigned int【参考方案2】:

@Barry 的回答为我指明了正确的方向。总是在列表中明确列出 pair 是不可取的。此外,我希望能够部分专门化map 的模板参数列表。考虑以下示例:

// for the sake of the example, suppose this works
constexpr map n(1, "1", 2, "2");
              // -> decltype(n) == map<int, const char*, 2>

// the following won't work
constexpr map<std::size_t, const char*> m(1, "1", 2, "2");

但是,也许用户希望地图包含 std::size_t 作为键,它没有文字。即他/她必须定义一个用户定义的文字才能做到这一点。

我们可以通过将工作卸载到 make_map 函数来解决这个问题,从而允许我们对地图进行部分特化:

// deduction guide for map's array constructor
template<class Key, class Value, std::size_t Size>
map(cxpair<Key, Value>(&&)[Size]) -> map<Key, Value, Size>;

// make_map builds the map
template<typename Key, typename Value, std::size_t Size>
constexpr auto make_map(cxpair<Key, Value>(&&m)[Size]) -> map<Key, Value, Size>
 return map<Key, Value, Size>(std::begin(m), std::end(m)); 

// allowing us to do:
constexpr auto mapr = make_map<int, std::string_view>( 1, "1",
                                                        2, "2",
                                                        3, "3" );

【讨论】:

以上是关于带有 initializer_list 的编译时查找表的主要内容,如果未能解决你的问题,请参考以下文章

巨大的 initializer_list 编译分段错误

从 initializer_list 初始化向量会产生不明确的编译错误

std::initializer_list 私有构造函数是不是从编译器得到非常特殊的处理?

在 Visual C++ 编译器中使用 std::initializer_list 2012 年 11 月 CTP

使用 std::initializer_list 创建指向 std::min 的函数指针

在向量映射上使用 initializer_list