From 2d93bbcd3d9d8d69493787ace0572dbf9af301cf Mon Sep 17 00:00:00 2001 From: Dustin Howett Date: Mon, 17 Aug 2020 16:21:10 -0700 Subject: [PATCH 1/4] Add til::static_map, a constexpr key-value store This is based on (cribbed almost directly from) code written by the inimitable @StephanTLavavej on one of our mailing lists. This is a nice generic version of the approach used in JsonUtils::EnumMapper and CodepointWidthDetector: a static array of key-value pairs that we binary-search at runtime (or at compile time, as the case may be.) Keys are not required to be sorted, as we're taking advantage of constexpr std::sort (VS 16.6+) to get the compiler to do it for us. How cool is that? static_map presents an operator[] or at much like std::map/std::unordered_map does. I've added some tests, but they're practically fully-solveable at compile time so they pretty much act like `VERIFY_IS_TRUE(true)`. --- src/inc/til/static_map.h | 57 ++++++++++++++ src/til/ut_til/StaticMapTests.cpp | 74 +++++++++++++++++++ src/til/ut_til/til.unit.tests.vcxproj | 1 + src/til/ut_til/til.unit.tests.vcxproj.filters | 1 + 4 files changed, 133 insertions(+) create mode 100644 src/inc/til/static_map.h create mode 100644 src/til/ut_til/StaticMapTests.cpp diff --git a/src/inc/til/static_map.h b/src/inc/til/static_map.h new file mode 100644 index 00000000000..f177c2ef846 --- /dev/null +++ b/src/inc/til/static_map.h @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +// static_map implements a very simple std::map-like type +// that is entirely (compile-time-)constant. +// There is no requirement that keys be sorted, as it will +// use constexpr std::sort during construction. + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + template, size_t N = 0> + class static_map + { + public: + template + constexpr explicit static_map(const Args&... args) : + _predicate{}, + _array{ { args... } } + { + static_assert(sizeof...(Args) == N); + const auto compareKeys = [&](const auto& p1, const auto& p2) { return _predicate(p1.first, p2.first); }; + std::sort(_array.begin(), _array.end(), compareKeys); // compile-time sorting! + } + + [[nodiscard]] constexpr const V& at(const K& key) const + { + const auto compareKey = [&](const auto& p) { return _predicate(p.first, key); }; + const auto iter{ std::partition_point(_array.begin(), _array.end(), compareKey) }; + + if (iter == _array.end() || _predicate(key, iter->first)) + { + throw std::runtime_error("key not found"); + } + + return iter->second; + } + + [[nodiscard]] constexpr const V& operator[](const K& key) const + { +#pragma warning(suppress : 26482) // Suppress bounds.2 check for indexing with constant expressions +#pragma warning(suppress : 26446) // Suppress bounds.4 check for subscript operator. + return (*this)[key]; + } + + private: + Compare _predicate; + std::array, N> _array; + }; + + // this is a deduction guide that ensures two things: + // 1. static_map's member types are all the same + // 2. static_map's fourth template argument (otherwise undeduced) is how many pairs it contains + template + static_map(First, Rest...) -> static_map...>, typename First::first_type, void>, typename First::second_type, std::less, 1 + sizeof...(Rest)>; +} diff --git a/src/til/ut_til/StaticMapTests.cpp b/src/til/ut_til/StaticMapTests.cpp new file mode 100644 index 00000000000..8c9f51f86e9 --- /dev/null +++ b/src/til/ut_til/StaticMapTests.cpp @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using namespace std::string_view_literals; + +class StaticMapTests +{ + TEST_CLASS(StaticMapTests); + + TEST_METHOD(Basic) + { + til::static_map intIntMap{ + std::pair{ 1, 100 }, + std::pair{ 3, 300 }, + std::pair{ 5, 500 }, + }; + + VERIFY_ARE_EQUAL(100, intIntMap.at(1)); + VERIFY_ARE_EQUAL(300, intIntMap.at(3)); + VERIFY_ARE_EQUAL(500, intIntMap.at(5)); + + int unused{ 0 }; + VERIFY_THROWS(unused = intIntMap.at(0), std::runtime_error); + VERIFY_THROWS(unused = intIntMap.at(7), std::runtime_error); + } + + TEST_METHOD(Unsorted) + { + til::static_map intIntMap{ + std::pair{ 5, 500 }, + std::pair{ 3, 300 }, + std::pair{ 1, 100 }, + }; + + VERIFY_ARE_EQUAL(100, intIntMap.at(1)); + VERIFY_ARE_EQUAL(300, intIntMap.at(3)); + VERIFY_ARE_EQUAL(500, intIntMap.at(5)); + + int unused{ 0 }; + VERIFY_THROWS(unused = intIntMap.at(0), std::runtime_error); + VERIFY_THROWS(unused = intIntMap.at(7), std::runtime_error); + } + + TEST_METHOD(StringViewKeys) + { + // We have to use the string view literal type here, as leaving the strings unviewed + // will result in a static_map + // Deduction guides are only applied when *no* template arguments are specified, + // which means we would need to specify them all, including the comparator and number of entries. + til::static_map stringIntMap{ + std::pair{ "xylophones"sv, 100 }, + std::pair{ "apples"sv, 200 }, + std::pair{ "grapes"sv, 300 }, + std::pair{ "pears"sv, 400 }, + }; + + VERIFY_ARE_EQUAL(100, stringIntMap.at("xylophones")); + VERIFY_ARE_EQUAL(300, stringIntMap.at("grapes")); + VERIFY_ARE_EQUAL(400, stringIntMap.at("pears")); + VERIFY_ARE_EQUAL(200, stringIntMap.at("apples")); + + int unused{ 0 }; + VERIFY_THROWS(unused = stringIntMap.at("0_hello"), std::runtime_error); + VERIFY_THROWS(unused = stringIntMap.at("z_world"), std::runtime_error); + } +}; diff --git a/src/til/ut_til/til.unit.tests.vcxproj b/src/til/ut_til/til.unit.tests.vcxproj index d25d9147f02..1b9975ec83b 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj +++ b/src/til/ut_til/til.unit.tests.vcxproj @@ -14,6 +14,7 @@ + diff --git a/src/til/ut_til/til.unit.tests.vcxproj.filters b/src/til/ut_til/til.unit.tests.vcxproj.filters index 5633f453b7d..924ee638336 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj.filters +++ b/src/til/ut_til/til.unit.tests.vcxproj.filters @@ -10,6 +10,7 @@ + From eb8503d34ecc53fbc72fb774f9869d11118b796a Mon Sep 17 00:00:00 2001 From: Dustin Howett Date: Mon, 17 Aug 2020 17:12:13 -0700 Subject: [PATCH 2/4] format --- src/inc/til/static_map.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inc/til/static_map.h b/src/inc/til/static_map.h index f177c2ef846..9996e32034e 100644 --- a/src/inc/til/static_map.h +++ b/src/inc/til/static_map.h @@ -53,5 +53,5 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // 1. static_map's member types are all the same // 2. static_map's fourth template argument (otherwise undeduced) is how many pairs it contains template - static_map(First, Rest...) -> static_map...>, typename First::first_type, void>, typename First::second_type, std::less, 1 + sizeof...(Rest)>; + static_map(First, Rest...)->static_map...>, typename First::first_type, void>, typename First::second_type, std::less, 1 + sizeof...(Rest)>; } From 10d967e14c8c5783cf5a73fc977d3a544be300e2 Mon Sep 17 00:00:00 2001 From: Dustin Howett Date: Tue, 18 Aug 2020 10:14:49 -0700 Subject: [PATCH 3/4] find, subscript, tests for both --- src/inc/til/static_map.h | 23 +++++++++++++++++++++-- src/til/ut_til/StaticMapTests.cpp | 30 +++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/inc/til/static_map.h b/src/inc/til/static_map.h index 9996e32034e..bd7dca7e528 100644 --- a/src/inc/til/static_map.h +++ b/src/inc/til/static_map.h @@ -14,6 +14,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" class static_map { public: + using const_iterator = typename std::array, N>::const_iterator; + template constexpr explicit static_map(const Args&... args) : _predicate{}, @@ -24,12 +26,29 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" std::sort(_array.begin(), _array.end(), compareKeys); // compile-time sorting! } - [[nodiscard]] constexpr const V& at(const K& key) const + [[nodiscard]] constexpr const_iterator find(const K& key) const noexcept { const auto compareKey = [&](const auto& p) { return _predicate(p.first, key); }; const auto iter{ std::partition_point(_array.begin(), _array.end(), compareKey) }; if (iter == _array.end() || _predicate(key, iter->first)) + { + return _array.end(); + } + + return iter; + } + + [[nodiscard]] constexpr const_iterator end() const noexcept + { + return _array.end(); + } + + [[nodiscard]] constexpr const V& at(const K& key) const + { + const auto iter{ find(key) }; + + if (iter == end()) { throw std::runtime_error("key not found"); } @@ -41,7 +60,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { #pragma warning(suppress : 26482) // Suppress bounds.2 check for indexing with constant expressions #pragma warning(suppress : 26446) // Suppress bounds.4 check for subscript operator. - return (*this)[key]; + return at(key); } private: diff --git a/src/til/ut_til/StaticMapTests.cpp b/src/til/ut_til/StaticMapTests.cpp index 8c9f51f86e9..3dade824a32 100644 --- a/src/til/ut_til/StaticMapTests.cpp +++ b/src/til/ut_til/StaticMapTests.cpp @@ -27,7 +27,7 @@ class StaticMapTests VERIFY_ARE_EQUAL(300, intIntMap.at(3)); VERIFY_ARE_EQUAL(500, intIntMap.at(5)); - int unused{ 0 }; + int unused{}; VERIFY_THROWS(unused = intIntMap.at(0), std::runtime_error); VERIFY_THROWS(unused = intIntMap.at(7), std::runtime_error); } @@ -44,7 +44,7 @@ class StaticMapTests VERIFY_ARE_EQUAL(300, intIntMap.at(3)); VERIFY_ARE_EQUAL(500, intIntMap.at(5)); - int unused{ 0 }; + int unused{}; VERIFY_THROWS(unused = intIntMap.at(0), std::runtime_error); VERIFY_THROWS(unused = intIntMap.at(7), std::runtime_error); } @@ -67,8 +67,32 @@ class StaticMapTests VERIFY_ARE_EQUAL(400, stringIntMap.at("pears")); VERIFY_ARE_EQUAL(200, stringIntMap.at("apples")); - int unused{ 0 }; + int unused{}; VERIFY_THROWS(unused = stringIntMap.at("0_hello"), std::runtime_error); VERIFY_THROWS(unused = stringIntMap.at("z_world"), std::runtime_error); } + + TEST_METHOD(Find) + { + til::static_map intIntMap{ + std::pair{ 5, 500 }, + }; + + VERIFY_ARE_NOT_EQUAL(intIntMap.end(), intIntMap.find(5)); + VERIFY_ARE_EQUAL(intIntMap.end(), intIntMap.find(7)); + } + +#pragma warning(push) +#pragma warning(disable : 26446) // Suppress bounds.4 check for subscript operator. + TEST_METHOD(Subscript) + { + til::static_map intIntMap{ + std::pair{ 5, 500 }, + }; + + VERIFY_ARE_EQUAL(500, intIntMap[5]); + int unused{}; + VERIFY_THROWS(unused = intIntMap[7], std::runtime_error); + } +#pragma warning(pop) }; From bd08596b02ee1d8d95194cb1b8317dcd62d34888 Mon Sep 17 00:00:00 2001 From: Dustin Howett Date: Tue, 18 Aug 2020 10:17:15 -0700 Subject: [PATCH 4/4] No need to suppress this --- src/inc/til/static_map.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/inc/til/static_map.h b/src/inc/til/static_map.h index bd7dca7e528..452e2dab36b 100644 --- a/src/inc/til/static_map.h +++ b/src/inc/til/static_map.h @@ -58,8 +58,6 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" [[nodiscard]] constexpr const V& operator[](const K& key) const { -#pragma warning(suppress : 26482) // Suppress bounds.2 check for indexing with constant expressions -#pragma warning(suppress : 26446) // Suppress bounds.4 check for subscript operator. return at(key); }