mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
Add some optimized data structures for maps on a small number of
keys, especially enumerated keys.
This commit is contained in:
203
include/swift/Basic/EnumMap.h
Normal file
203
include/swift/Basic/EnumMap.h
Normal file
@@ -0,0 +1,203 @@
|
||||
//===--- EnumMap.h - A map optimized for having enum keys -------*- C++ -*-===//
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
///
|
||||
/// This file defines the EnumMap class template, which is a map data
|
||||
/// structure optimized for working with enumerated keys. It is built on
|
||||
/// top of SmallMap, but it replaces the default large map with a flat
|
||||
/// heap-allocated array of indexes into the elements, which is reasonable
|
||||
/// for small-ish enums.
|
||||
///
|
||||
/// Currently the map requires the key type to be an enum type.
|
||||
/// The expectation is that the enum has a small number of enumerators
|
||||
/// which are all in the range 0..<NumValues. NumValues must be provided
|
||||
/// by specializing the EnumTraits class.
|
||||
///
|
||||
/// The elements of the map remain insertion-ordered for the lifetime of
|
||||
/// the map. There are currently no operations to remove elements.
|
||||
/// Iterators are invalidated by insertion.
|
||||
///
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef SWIFT_BASIC_ENUMMAP_H
|
||||
#define SWIFT_BASIC_ENUMMAP_H
|
||||
|
||||
#include "swift/Basic/EnumTraits.h"
|
||||
#include "swift/Basic/SmallMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include <type_traits>
|
||||
|
||||
namespace swift {
|
||||
|
||||
/// The maximum number of elements that the map can have before
|
||||
/// it flips from brute-force searching the keys to using a sparse
|
||||
/// array.
|
||||
static constexpr size_t DefaultEnumMapDirectSearchLimit =
|
||||
DefaultSmallMapDirectSearchLimit;
|
||||
|
||||
/// The primary customization point for an EnumMap.
|
||||
///
|
||||
/// template <>
|
||||
/// struct EnumMapTraits<MyKey> {
|
||||
/// using IndexType = <some integer type>;
|
||||
/// struct LargeMapStorage {
|
||||
/// std::optional<IndexType> find(IndexType) const;
|
||||
/// std::pair<IndexType, bool> insert(IndexType key, IndexType value);
|
||||
/// };
|
||||
/// };
|
||||
template <class Key, class KeyTraits = EnumTraits<Key>>
|
||||
struct EnumMapTraits;
|
||||
|
||||
template <class Key, class Value,
|
||||
size_t DirectSearchLimit = DefaultEnumMapDirectSearchLimit,
|
||||
class MapTraits = EnumMapTraits<Key>,
|
||||
class ElementStorage = llvm::SmallVector<Value>>
|
||||
class EnumMap {
|
||||
using IndexType = typename MapTraits::IndexType;
|
||||
|
||||
// EnumMapTraits is currently designed to be usable directly as a
|
||||
// SmallMapTraits.
|
||||
using MapType =
|
||||
SmallMap<IndexType, Value, DirectSearchLimit, MapTraits, ElementStorage>;
|
||||
MapType map;
|
||||
|
||||
public:
|
||||
bool empty() const { return map.empty(); }
|
||||
size_t size() const { return map.size(); }
|
||||
|
||||
using iterator = typename MapType::iterator;
|
||||
iterator begin() { return map.begin(); }
|
||||
iterator end() { return map.end(); }
|
||||
|
||||
using const_iterator = typename MapType::const_iterator;
|
||||
const_iterator begin() const { return map.begin(); }
|
||||
const_iterator end() const { return map.end(); }
|
||||
|
||||
/// Look up a key in the map. Returns end() if the entry is not found.
|
||||
const_iterator find(Key key) const {
|
||||
return map.find(IndexType(key));
|
||||
}
|
||||
|
||||
/// Try to insert the given key/value pair. If there's already an element
|
||||
/// with this key, return false and an iterator for the existing element.
|
||||
/// Otherwise, return true and an iterator for the new element.
|
||||
///
|
||||
/// The value in the set will be constructed by emplacing it with the
|
||||
/// given arguments.
|
||||
template <class... Args>
|
||||
std::pair<iterator, bool> insert(Key key, Args &&...valueArgs) {
|
||||
return map.insert(IndexType(key), std::forward<Args>(valueArgs)...);
|
||||
}
|
||||
};
|
||||
|
||||
namespace EnumMapImpl {
|
||||
|
||||
template <size_t N,
|
||||
bool SmallEnoughForUInt8 = (N < (1U << 8)),
|
||||
bool SmallEnoughForUInt16 = (N < (1U << 16))>
|
||||
struct SufficientIntFor;
|
||||
|
||||
template <size_t N>
|
||||
struct SufficientIntFor<N, true, true> {
|
||||
using type = uint8_t;
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
struct SufficientIntFor<N, false, true> {
|
||||
using type = uint16_t;
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
struct SufficientIntFor<N, false, false> {
|
||||
static_assert(N < (1ULL << 32), "just how large is this \"enum\" exactly");
|
||||
using type = uint32_t;
|
||||
};
|
||||
|
||||
/// A map from integers in 0..<N to integers in 0..<N, implemented as a
|
||||
/// flat array of integers in 0...N, with zero meaning a missing entry.
|
||||
///
|
||||
/// This is a great implementation for N <= 255, where the
|
||||
/// entire flat array is <= 256 bytes. It gets increasingly marginal
|
||||
/// for N up to ~1K or so (unless we really expect to have entries
|
||||
/// for a large proportion of the enum). Past that, we should probably
|
||||
/// be falling back on something like a hashtable, because needing tens
|
||||
/// of kilobytes to hold as few as 17 entries is objectively unreasonable.
|
||||
template <size_t N>
|
||||
class FlatMap {
|
||||
public:
|
||||
using IndexType = typename SufficientIntFor<N>::type;
|
||||
using StoredIndexType = typename SufficientIntFor<N + 1>::type;
|
||||
|
||||
private:
|
||||
StoredIndexType *ptr;
|
||||
|
||||
public:
|
||||
FlatMap() : ptr(new StoredIndexType[N]) {
|
||||
memset(ptr, 0, N * sizeof(StoredIndexType));
|
||||
}
|
||||
FlatMap(FlatMap &&other)
|
||||
: ptr(other.ptr) {
|
||||
other.ptr = nullptr;
|
||||
}
|
||||
FlatMap &operator=(FlatMap &&other) {
|
||||
delete ptr;
|
||||
ptr = other.ptr;
|
||||
other.ptr = nullptr;
|
||||
}
|
||||
FlatMap(const FlatMap &other)
|
||||
: ptr(new StoredIndexType[N]) {
|
||||
memcpy(ptr, other.ptr, N * sizeof(StoredIndexType));
|
||||
}
|
||||
FlatMap &operator=(const FlatMap &other) {
|
||||
memcpy(ptr, other.ptr, N * sizeof(StoredIndexType));
|
||||
}
|
||||
|
||||
~FlatMap() {
|
||||
delete ptr;
|
||||
}
|
||||
|
||||
std::pair<IndexType, bool> insert(IndexType key, IndexType value) {
|
||||
assert(key < N);
|
||||
StoredIndexType &entry = ptr[key];
|
||||
if (entry == 0) {
|
||||
entry = StoredIndexType(value) + 1;
|
||||
return std::make_pair(value, true);
|
||||
} else {
|
||||
return std::make_pair(IndexType(entry - 1), false);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<IndexType> find(IndexType key) const {
|
||||
assert(key < N);
|
||||
StoredIndexType entry = ptr[key];
|
||||
if (entry == 0) {
|
||||
return std::nullopt;
|
||||
} else {
|
||||
return IndexType(entry - 1);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} // end namespace EnumMapImpl
|
||||
|
||||
/// The default implementation of EnumMapTraits.
|
||||
template <class Key_, class KeyTraits_>
|
||||
struct EnumMapTraits {
|
||||
using Key = Key_;
|
||||
using KeyTraits = KeyTraits_;
|
||||
|
||||
using LargeMapStorage = EnumMapImpl::FlatMap<KeyTraits::NumValues>;
|
||||
using IndexType = typename LargeMapStorage::IndexType;
|
||||
};
|
||||
|
||||
} // end namespace swift
|
||||
|
||||
#endif
|
||||
34
include/swift/Basic/EnumTraits.h
Normal file
34
include/swift/Basic/EnumTraits.h
Normal file
@@ -0,0 +1,34 @@
|
||||
//===--- EnumTraits.h - Traits for densely-packed enums ---------*- C++ -*-===//
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
///
|
||||
/// This file defines the EnumTraits concept, which can be used to
|
||||
/// communicate information about an enum type's enumerators that currently
|
||||
/// can't be recovered from the compiler.
|
||||
///
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef SWIFT_BASIC_ENUMTRAITS_H
|
||||
#define SWIFT_BASIC_ENUMTRAITS_H
|
||||
|
||||
namespace swift {
|
||||
|
||||
/// A simple traits concept for recording the number of cases in an enum.
|
||||
///
|
||||
/// template <> class EnumTraits<WdigetKind> {
|
||||
/// static constexpr size_t NumValues = NumWidgetKinds;
|
||||
/// };
|
||||
template <class E>
|
||||
struct EnumTraits;
|
||||
|
||||
} // end namespace swift
|
||||
|
||||
#endif
|
||||
352
include/swift/Basic/SmallMap.h
Normal file
352
include/swift/Basic/SmallMap.h
Normal file
@@ -0,0 +1,352 @@
|
||||
//===--- SmallMap.h - A map optimized for having few entries ----*- C++ -*-===//
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
///
|
||||
/// This file defines the SmallMap data structure, which is optimized for
|
||||
/// a small number of keys. The values of the map are stored in a dynamic
|
||||
/// array (such a SmallVector). Iterating the map iterates these values
|
||||
/// in insertion order. If the number of entries is small (not more than
|
||||
/// the "direct search limit"), the keys are stored in an inline array
|
||||
/// that is parallel to the elements array, and lookups brute-force search
|
||||
/// this array for the key and then use the element with the same index. If
|
||||
/// the number of entries grows beyond that limit, the map fall back to a
|
||||
/// "large" map of keys to indexes, which defaults to a DenseMap<Key, size_t>.
|
||||
///
|
||||
/// There are currently no operations to remove elements.
|
||||
/// Iterators are invalidated by insertion.
|
||||
///
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef SWIFT_BASIC_SMALLMAP_H
|
||||
#define SWIFT_BASIC_SMALLMAP_H
|
||||
|
||||
#include "swift/Basic/Range.h"
|
||||
#include "swift/Basic/UninitializedArray.h"
|
||||
#include "llvm/ADT/DenseMap.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace swift {
|
||||
|
||||
/// The maximum number of elements that the map can have before
|
||||
/// it flips from brute-force searching the keys to using the
|
||||
/// large map structure.
|
||||
static constexpr size_t DefaultSmallMapDirectSearchLimit = 16;
|
||||
|
||||
/// The primary customization point for a SmallMap.
|
||||
///
|
||||
/// template <>
|
||||
/// struct SmallMapTraits<MyKey> {
|
||||
/// using IndexType = <some integer type>;
|
||||
/// struct LargeMapStorage {
|
||||
/// std::optional<IndexType> find(const MyKey &key) const;
|
||||
/// std::pair<IndexType, bool> insert(MyKey &&key, IndexType value);
|
||||
/// };
|
||||
/// };
|
||||
template <class Key>
|
||||
struct SmallMapTraits;
|
||||
|
||||
template <class Key, class Value,
|
||||
size_t DirectSearchLimit = DefaultSmallMapDirectSearchLimit,
|
||||
class MapTraits = SmallMapTraits<Key>,
|
||||
class ElementStorage = llvm::SmallVector<Value>>
|
||||
class SmallMap {
|
||||
using IndexType = typename MapTraits::IndexType;
|
||||
using LargeMapStorage = typename MapTraits::LargeMapStorage;
|
||||
using SmallMapStorage = UninitializedArray<Key, DirectSearchLimit>;
|
||||
|
||||
static_assert(std::is_integral_v<IndexType>,
|
||||
"index type must be an integer type");
|
||||
|
||||
ElementStorage elements;
|
||||
|
||||
union {
|
||||
LargeMapStorage largeMap;
|
||||
SmallMapStorage smallMap;
|
||||
};
|
||||
|
||||
bool isLarge() const {
|
||||
// This only works because there are no operations to remove entries.
|
||||
return elements.size() > DirectSearchLimit;
|
||||
}
|
||||
|
||||
template <class... Args>
|
||||
void initializeLargeMap(Args &&...args) {
|
||||
::new ((void*) &largeMap) LargeMapStorage(std::forward<Args>(args)...);
|
||||
}
|
||||
void destroyLargeMap() {
|
||||
largeMap.~LargeMapStorage();
|
||||
}
|
||||
|
||||
void initializeSmallMap() {
|
||||
::new ((void*) &smallMap) SmallMapStorage();
|
||||
}
|
||||
void destroySmallMap(size_t numElements) {
|
||||
smallMap.destroy(numElements);
|
||||
smallMap.~SmallMapStorage();
|
||||
}
|
||||
|
||||
public:
|
||||
SmallMap() {
|
||||
initializeSmallMap();
|
||||
}
|
||||
|
||||
SmallMap(SmallMap &&other)
|
||||
: elements(std::move(other.elements)) {
|
||||
|
||||
// Make sure that the other object has an element count of zero.
|
||||
other.elements.clear();
|
||||
assert(!other.isLarge());
|
||||
|
||||
auto newSize = size();
|
||||
bool newIsLarge = isLarge();
|
||||
|
||||
// Destructively move the other object's map storage to this object.
|
||||
// Postcondition: the other object's map storage is in the small
|
||||
// map state with zero initialized objects.
|
||||
if (newIsLarge) {
|
||||
initializeLargeMap(std::move(other.largeMap));
|
||||
other.destroyLargeMap();
|
||||
other.initializeSmallMap();
|
||||
} else {
|
||||
initializeSmallMap();
|
||||
smallMap.destructiveMoveInitialize(std::move(other.smallMap), newSize);
|
||||
}
|
||||
}
|
||||
|
||||
SmallMap(const SmallMap &other)
|
||||
: elements(other.elements) {
|
||||
auto newSize = size();
|
||||
bool newIsLarge = isLarge();
|
||||
if (newIsLarge) {
|
||||
initializeLargeMap(other.largeMap);
|
||||
} else {
|
||||
initializeSmallMap();
|
||||
smallMap.copyInitialize(other.smallMap, newSize);
|
||||
}
|
||||
}
|
||||
|
||||
SmallMap &operator=(SmallMap &&other) {
|
||||
size_t oldSize = size();
|
||||
bool oldIsLarge = isLarge();
|
||||
elements = std::move(other.elements);
|
||||
size_t newSize = size();
|
||||
bool newIsLarge = isLarge();
|
||||
|
||||
// Make sure that the other object has an element count of zero.
|
||||
other.elements.clear();
|
||||
assert(!other.isLarge());
|
||||
|
||||
// Move the other object's map storage to this object.
|
||||
// Postcondition: the other object's map storage is in the small
|
||||
// map state with zero initialized objects.
|
||||
|
||||
// large -> large
|
||||
if (oldIsLarge && newIsLarge) {
|
||||
largeMap = std::move(other.largeMap);
|
||||
other.destroyLargeMap();
|
||||
other.initializeSmallMap();
|
||||
|
||||
// large -> small
|
||||
} else if (oldIsLarge) {
|
||||
destroyLargeMap();
|
||||
initializeSmallMap();
|
||||
smallMap.destructiveMoveInitialize(std::move(other.smallMap), newSize);
|
||||
|
||||
// small -> large
|
||||
} else if (newIsLarge) {
|
||||
destroySmallMap(oldSize);
|
||||
initializeLargeMap(std::move(other.largeMap));
|
||||
other.destroyLargeMap();
|
||||
other.initializeSmallMap();
|
||||
|
||||
// small -> small
|
||||
} else {
|
||||
smallMap.destructiveMoveAssign(std::move(other.smallMap), oldSize, newSize);
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
SmallMap &operator=(const SmallMap &other) {
|
||||
size_t oldSize = size();
|
||||
bool oldIsLarge = isLarge();
|
||||
|
||||
// Copy the other object's elements to this object.
|
||||
elements = other.elements;
|
||||
|
||||
size_t newSize = size();
|
||||
bool newIsLarge = isLarge();
|
||||
|
||||
// Copy the other object's map storage to this object:
|
||||
|
||||
// large -> large
|
||||
if (oldIsLarge && newIsLarge) {
|
||||
largeMap = other.largeMap;
|
||||
|
||||
// large -> small
|
||||
} else if (oldIsLarge) {
|
||||
destroyLargeMap();
|
||||
initializeSmallMap();
|
||||
smallMap.copyInitialize(other.smallMap, newSize);
|
||||
|
||||
// small -> large
|
||||
} else if (newIsLarge) {
|
||||
destroySmallMap(oldSize);
|
||||
initializeLargeMap(other.largeMap);
|
||||
|
||||
// small -> small
|
||||
} else {
|
||||
smallMap.copyAssign(other.smallMap, oldSize, newSize);
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
~SmallMap() {
|
||||
if (isLarge()) {
|
||||
destroyLargeMap();
|
||||
} else {
|
||||
destroySmallMap(size());
|
||||
}
|
||||
}
|
||||
|
||||
bool empty() const { return elements.empty(); }
|
||||
size_t size() const { return elements.size(); }
|
||||
|
||||
using iterator = typename ElementStorage::iterator;
|
||||
iterator begin() { return elements.begin(); }
|
||||
iterator end() { return elements.end(); }
|
||||
|
||||
using const_iterator = typename ElementStorage::const_iterator;
|
||||
const_iterator begin() const { return elements.begin(); }
|
||||
const_iterator end() const { return elements.end(); }
|
||||
|
||||
/// Look up a key in the map. Returns end() if the entry is not found.
|
||||
const_iterator find(const Key &key) const {
|
||||
if (isLarge()) {
|
||||
std::optional<IndexType> result = largeMap.find(key);
|
||||
if (result)
|
||||
return elements.begin() + *result;
|
||||
return elements.end();
|
||||
}
|
||||
|
||||
size_t n = elements.size();
|
||||
for (size_t i : range(n))
|
||||
if (smallMap[i] == key)
|
||||
return elements.begin() + i;
|
||||
|
||||
return elements.end();
|
||||
}
|
||||
|
||||
/// Try to insert the given key/value pair. If there's already an element
|
||||
/// with this key, return false and an iterator for the existing element.
|
||||
/// Otherwise, return true and an iterator for the new element.
|
||||
///
|
||||
/// The value in the set will be constructed by emplacing it with the
|
||||
/// given arguments.
|
||||
template <class KeyT, class... Args>
|
||||
std::pair<iterator, bool> insert(KeyT &&key, Args &&...valueArgs) {
|
||||
// The current number of elements, and therefore also the index of
|
||||
// the new element if we create one.
|
||||
auto n = elements.size();
|
||||
|
||||
if (isLarge()) {
|
||||
// Try to insert a map entry pointing to the potential new element.
|
||||
auto result = largeMap.insert(std::forward<KeyT>(key), n);
|
||||
|
||||
// If we successfully inserted, emplace the new element.
|
||||
if (result.second) {
|
||||
assert(result.first == n);
|
||||
elements.emplace_back(std::forward<Args>(valueArgs)...);
|
||||
return {elements.begin() + n, true};
|
||||
}
|
||||
|
||||
// Otherwise, return the existing value.
|
||||
return {elements.begin() + result.first, false};
|
||||
}
|
||||
|
||||
// Search the small map for the key.
|
||||
for (size_t i : range(n))
|
||||
if (smallMap[i] == key)
|
||||
return {elements.begin() + i, false};
|
||||
|
||||
// If that didn't match, we have to insert. Emplace the new element.
|
||||
elements.emplace_back(std::forward<Args>(valueArgs)...);
|
||||
|
||||
// If we aren't crossing the large-map threshold, just emplace the
|
||||
// new key.
|
||||
if (n < DirectSearchLimit) {
|
||||
smallMap.emplace(n, std::forward<KeyT>(key));
|
||||
return {elements.begin() + n, true};
|
||||
}
|
||||
|
||||
// Otherwise, we need to transition the map from small to large.
|
||||
|
||||
// Move the small map aside.
|
||||
assert(isLarge());
|
||||
SmallMapStorage smallMapCopy;
|
||||
smallMapCopy.destructiveMoveInitialize(std::move(smallMap), n);
|
||||
destroySmallMap(0); // formally end lifetime
|
||||
|
||||
// Initialize the large map with the existing mappings taken from
|
||||
// the moved-aside small map.
|
||||
initializeLargeMap();
|
||||
for (size_t i : range(n)) {
|
||||
auto result = largeMap.insert(std::move(smallMapCopy[i]), i);
|
||||
assert(result.second && result.first == i); (void) result;
|
||||
}
|
||||
|
||||
// Add the new mapping.
|
||||
auto result = largeMap.insert(std::forward<KeyT>(key), n);
|
||||
assert(result.second && result.first == n); (void) result;
|
||||
|
||||
// Destroy the elements of the copied small map, which we moved
|
||||
// into the large map but didn't *destructively* move.
|
||||
smallMapCopy.destroy(n);
|
||||
|
||||
return {elements.begin() + n, true};
|
||||
}
|
||||
};
|
||||
|
||||
namespace SmallMapImpl {
|
||||
|
||||
template <class Key>
|
||||
struct DefaultSmallMapTraits {
|
||||
using IndexType = size_t;
|
||||
|
||||
struct LargeMapStorage {
|
||||
llvm::DenseMap<Key, IndexType> map;
|
||||
|
||||
std::optional<IndexType> find(const Key &key) const {
|
||||
auto it = map.find(key);
|
||||
if (it == map.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
template <class KeyArg>
|
||||
std::pair<IndexType, bool> insert(KeyArg &&key, IndexType value) {
|
||||
auto result = map.insert(std::make_pair(std::forward<KeyArg>(key), value));
|
||||
return std::make_pair(result.first->second, result.second);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} // end namespace SmallMapImpl
|
||||
|
||||
template <class Key>
|
||||
struct SmallMapTraits : SmallMapImpl::DefaultSmallMapTraits<Key> {};
|
||||
|
||||
} // end namespace swift
|
||||
|
||||
#endif
|
||||
156
include/swift/Basic/UninitializedArray.h
Normal file
156
include/swift/Basic/UninitializedArray.h
Normal file
@@ -0,0 +1,156 @@
|
||||
//===--- UninitializedArray.h - Array of uninitialized objects --*- C++ -*-===//
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
///
|
||||
/// This file defines the UninitializedArray "data structure", which
|
||||
/// can hold an uninitialized array of values and provides explicit
|
||||
/// operations to copy, move, and destroy them.
|
||||
///
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef SWIFT_BASIC_UNINITIALIZEDARRAY_H
|
||||
#define SWIFT_BASIC_UNINITIALIZEDARRAY_H
|
||||
|
||||
#include <assert.h>
|
||||
#include <memory>
|
||||
|
||||
namespace swift {
|
||||
|
||||
/// An array of uninitialized elements. The user is responsible for
|
||||
/// ensuring that it's used properly.
|
||||
template <class T, size_t N>
|
||||
class UninitializedArray {
|
||||
union {
|
||||
T elements[N];
|
||||
};
|
||||
|
||||
public:
|
||||
UninitializedArray() {}
|
||||
UninitializedArray(const UninitializedArray &other) = delete;
|
||||
UninitializedArray &operator=(const UninitializedArray &other) = delete;
|
||||
UninitializedArray(UninitializedArray &&other) = delete;
|
||||
UninitializedArray &operator=(UninitializedArray &&other) = delete;
|
||||
~UninitializedArray() {}
|
||||
|
||||
using iterator = T *;
|
||||
using const_iterator = const T *;
|
||||
iterator begin() { return elements; }
|
||||
const_iterator begin() const { return elements; }
|
||||
// We intentionally don't provide end() because it's too easy to use it
|
||||
// accidentally when there's no guarantee that those elements exist.
|
||||
|
||||
template <class... Args>
|
||||
T &emplace(size_t i, Args &&...args) {
|
||||
assert(i < N);
|
||||
return *::new ((void*) &elements[i]) T(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
T &operator[](size_t i) {
|
||||
assert(i < N);
|
||||
return elements[i];
|
||||
}
|
||||
|
||||
const T &operator[](size_t i) const {
|
||||
assert(i < N);
|
||||
return elements[i];
|
||||
}
|
||||
|
||||
/// Given that this array contains no initialized elements and the other
|
||||
/// array contains at least newSize initialized elements, fill this array
|
||||
/// with newSize initialized elements copied from the other array.
|
||||
void copyInitialize(const UninitializedArray &other, size_t newSize) {
|
||||
assert(newSize <= N);
|
||||
std::uninitialized_copy(other.begin(), other.begin() + newSize, begin());
|
||||
}
|
||||
|
||||
/// Given that this array contains oldSize initialized elements and the other
|
||||
/// array contains at least newSize initialized elements, fill this array
|
||||
/// with newSize initialized elements copied from the other array.
|
||||
void copyAssign(const UninitializedArray &other,
|
||||
size_t oldSize, size_t newSize) {
|
||||
assert(oldSize <= N);
|
||||
assert(newSize <= N);
|
||||
|
||||
auto commonSize = std::min(oldSize, newSize);
|
||||
auto thisBegin = begin();
|
||||
auto otherBegin = other.begin();
|
||||
|
||||
// Copy-assign the common prefix.
|
||||
std::copy(otherBegin, otherBegin + commonSize, thisBegin);
|
||||
|
||||
// If there are more elements in the other array, copy-initialize those
|
||||
// elements into this array starting after the common prefix.
|
||||
if (oldSize < newSize) {
|
||||
std::uninitialized_copy(otherBegin + commonSize, otherBegin + newSize,
|
||||
thisBegin + commonSize);
|
||||
|
||||
// Otherwise, if there were more elements in this array, destroy the
|
||||
// excess elements.
|
||||
} else if (oldSize > newSize) {
|
||||
std::destroy(thisBegin + commonSize, thisBegin + oldSize);
|
||||
}
|
||||
}
|
||||
|
||||
/// Given that this array contains no initialized elements and the other
|
||||
/// array contains exactly newSize initialized elements, fill this array
|
||||
/// with newSize initialized elements destructively moved from the other
|
||||
/// array. The other array is left with no initialized elements.
|
||||
void destructiveMoveInitialize(UninitializedArray &&other, size_t newSize) {
|
||||
assert(newSize <= N);
|
||||
auto it = std::move_iterator(other.begin());
|
||||
std::uninitialized_copy(it, it + newSize, begin());
|
||||
std::destroy(other.begin(), other.begin() + newSize);
|
||||
}
|
||||
|
||||
/// Given that this array contains oldSize initialized elements and the other
|
||||
/// array contains exactly newSize initialized elements, fill this array with
|
||||
/// newSize initialized elements destructively moved from the other array.
|
||||
/// The other array is left with no initialized elements.
|
||||
void destructiveMoveAssign(UninitializedArray &&other,
|
||||
size_t oldSize, size_t newSize) {
|
||||
assert(oldSize <= N);
|
||||
assert(newSize <= N);
|
||||
|
||||
auto commonSize = std::min(oldSize, newSize);
|
||||
auto thisBegin = begin();
|
||||
auto otherBegin = std::move_iterator(other.begin());
|
||||
|
||||
// Move-assign the common prefix. Note that we use a move_iterator to
|
||||
// cause all these "copies" to be moves.
|
||||
std::copy(otherBegin, otherBegin + commonSize, thisBegin);
|
||||
|
||||
// If there are more elements in the new array, move-initialize those
|
||||
// elements starting after the common prefix.
|
||||
if (oldSize < newSize) {
|
||||
std::uninitialized_copy(otherBegin + commonSize, otherBegin + newSize,
|
||||
thisBegin + commonSize);
|
||||
|
||||
// Otherwise, if there were more elements in this array, destroy the
|
||||
// excess elements.
|
||||
} else if (oldSize > newSize) {
|
||||
std::destroy(thisBegin + commonSize, thisBegin + oldSize);
|
||||
}
|
||||
|
||||
// Destroy all of the elements in the other array.
|
||||
std::destroy(other.begin(), other.begin() + oldSize);
|
||||
}
|
||||
|
||||
/// Given thait this array contains exactly oldSize initialized elements,
|
||||
/// destroy those elements, leaving it with no initialized elements.
|
||||
void destroy(size_t oldSize) {
|
||||
assert(oldSize <= N);
|
||||
std::destroy(begin(), begin() + oldSize);
|
||||
}
|
||||
};
|
||||
|
||||
} // end namespace swift
|
||||
|
||||
#endif
|
||||
@@ -14,6 +14,7 @@ add_swift_unittest(SwiftBasicTests
|
||||
DemangleTest.cpp
|
||||
DiverseStackTest.cpp
|
||||
EditorPlaceholderTest.cpp
|
||||
EnumMapTest.cpp
|
||||
EncodedSequenceTest.cpp
|
||||
ExponentialGrowthAppendingBinaryByteStreamTests.cpp
|
||||
FileSystemTest.cpp
|
||||
@@ -28,6 +29,7 @@ add_swift_unittest(SwiftBasicTests
|
||||
PointerIntEnumTest.cpp
|
||||
PrefixMapTest.cpp
|
||||
RangeTest.cpp
|
||||
SmallMapTest.cpp
|
||||
SourceManagerTest.cpp
|
||||
StableHasher.cpp
|
||||
STLExtrasTest.cpp
|
||||
|
||||
154
unittests/Basic/EnumMapTest.cpp
Normal file
154
unittests/Basic/EnumMapTest.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
#include "swift/Basic/EnumMap.h"
|
||||
#include "llvm/ADT/DenseSet.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace swift;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MissingValue = ~(size_t) 0;
|
||||
|
||||
enum class A : uint16_t {
|
||||
lowerBound = 0,
|
||||
numValues = 1000
|
||||
};
|
||||
|
||||
} // end anonymous namespace
|
||||
|
||||
template <>
|
||||
struct swift::EnumTraits<A> {
|
||||
static constexpr size_t NumValues = (size_t) A::numValues;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
static void insertSuccess(EnumMap<A, size_t> &map, size_t key, size_t value) {
|
||||
auto result = map.insert(A(key), value);
|
||||
EXPECT_TRUE(result.second);
|
||||
EXPECT_NE(map.end(), result.first);
|
||||
EXPECT_EQ(value, *result.first);
|
||||
}
|
||||
|
||||
static void insertFailure(EnumMap<A, size_t> &map, size_t key, size_t value,
|
||||
size_t actualValue) {
|
||||
auto result = map.insert(A(key), value);
|
||||
EXPECT_FALSE(result.second);
|
||||
EXPECT_NE(map.end(), result.first);
|
||||
EXPECT_EQ(actualValue, *result.first);
|
||||
}
|
||||
|
||||
static void lookupSuccess(EnumMap<A, size_t> &map, size_t key, size_t value) {
|
||||
auto result = map.find(A(key));
|
||||
EXPECT_NE(map.end(), result);
|
||||
EXPECT_EQ(value, *result);
|
||||
}
|
||||
|
||||
static void lookupFailure(EnumMap<A, size_t> &map, size_t key) {
|
||||
auto result = map.find(A(key));
|
||||
EXPECT_EQ(map.end(), result);
|
||||
}
|
||||
|
||||
#define INSERT_SUCCESS(KEY, VALUE) \
|
||||
insertSuccess(map, KEY, VALUE)
|
||||
#define INSERT_FAILURE(KEY, VALUE, ACTUAL) \
|
||||
insertFailure(map, KEY, VALUE, ACTUAL)
|
||||
#define LOOKUP_SUCCESS(KEY, VALUE) \
|
||||
lookupSuccess(map, KEY, VALUE)
|
||||
#define LOOKUP_FAILURE(KEY) \
|
||||
lookupFailure(map, KEY)
|
||||
|
||||
struct entry {
|
||||
size_t key;
|
||||
size_t value;
|
||||
};
|
||||
|
||||
static const entry globalEntries[] = {
|
||||
{ 218, 110145 },
|
||||
{ 361, 927012 },
|
||||
{ 427, 608227 },
|
||||
{ 861, 158552 },
|
||||
{ 101, 466452 },
|
||||
{ 391, 920472 },
|
||||
{ 960, 522979 },
|
||||
{ 36, 433291 },
|
||||
{ 432, 110883 },
|
||||
{ 752, 903125 },
|
||||
{ 549, 887829 },
|
||||
{ 475, 748953 },
|
||||
{ 295, 214526 },
|
||||
{ 533, 896211 },
|
||||
{ 961, 684099 },
|
||||
{ 230, 387362 },
|
||||
{ 988, 205038 },
|
||||
{ 980, 838945 },
|
||||
{ 43, 319398 },
|
||||
{ 704, 960347 },
|
||||
{ 270, 837198 },
|
||||
{ 611, 310181 },
|
||||
{ 638, 44564 },
|
||||
{ 193, 210584 },
|
||||
{ 281, 620103 },
|
||||
{ 682, 462845 },
|
||||
{ 419, 85019 },
|
||||
{ 812, 541739 },
|
||||
{ 580, 266684 },
|
||||
{ 559, 101634 },
|
||||
{ 506, 639451 },
|
||||
{ 96, 782184 },
|
||||
{ 996, 927190 },
|
||||
{ 392, 586071 },
|
||||
{ 928, 50086 },
|
||||
{ 976, 681150 },
|
||||
{ 953, 172478 },
|
||||
{ 863, 512828 },
|
||||
{ 569, 947708 },
|
||||
{ 139, 131866 },
|
||||
{ 628, 884682 },
|
||||
{ 877, 636903 },
|
||||
{ 49, 871169 },
|
||||
{ 172, 524694 },
|
||||
{ 768, 211821 },
|
||||
{ 104, 126356 },
|
||||
{ 552, 262470 },
|
||||
{ 343, 857409 },
|
||||
{ 426, 535485 },
|
||||
{ 84, 954703 },
|
||||
{ 239, 889527 },
|
||||
};
|
||||
|
||||
} // end anonymous namespace
|
||||
|
||||
|
||||
TEST(EnumMap, Basic) {
|
||||
EnumMap<A, size_t> map;
|
||||
|
||||
auto entries = llvm::makeArrayRef(globalEntries);
|
||||
|
||||
for (size_t iteration : range(entries.size())) {
|
||||
EXPECT_EQ(iteration, map.size());
|
||||
EXPECT_EQ(iteration == 0, map.empty());
|
||||
|
||||
// Check that previous entries are still there.
|
||||
for (size_t i : range(iteration)) {
|
||||
LOOKUP_SUCCESS(entries[i].key, entries[i].value);
|
||||
INSERT_FAILURE(entries[i].key, MissingValue, entries[i].value);
|
||||
}
|
||||
|
||||
// Check that later entries are not there.
|
||||
for (size_t i : range(iteration, entries.size())) {
|
||||
LOOKUP_FAILURE(entries[i].key);
|
||||
}
|
||||
|
||||
INSERT_SUCCESS(entries[iteration].key, entries[iteration].value);
|
||||
LOOKUP_SUCCESS(entries[iteration].key, entries[iteration].value);
|
||||
}
|
||||
|
||||
EXPECT_EQ(entries.size(), map.size());
|
||||
|
||||
size_t i = 0;
|
||||
for (auto &value : map) {
|
||||
EXPECT_EQ(entries[i].value, value);
|
||||
i++;
|
||||
}
|
||||
EXPECT_EQ(entries.size(), i);
|
||||
}
|
||||
273
unittests/Basic/SmallMapTest.cpp
Normal file
273
unittests/Basic/SmallMapTest.cpp
Normal file
@@ -0,0 +1,273 @@
|
||||
#include "swift/Basic/SmallMap.h"
|
||||
#include "llvm/ADT/DenseSet.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace swift;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MissingValue = ~(size_t) 0;
|
||||
constexpr size_t EmptyKey = ~(size_t) 1;
|
||||
constexpr size_t TombstoneKey = ~(size_t) 2;
|
||||
|
||||
template <class T>
|
||||
struct Tracker {
|
||||
llvm::DenseSet<T> set;
|
||||
|
||||
Tracker() = default;
|
||||
Tracker(const Tracker &) = delete;
|
||||
Tracker &operator=(const Tracker &) = delete;
|
||||
|
||||
bool empty() const {
|
||||
return set.empty();
|
||||
}
|
||||
|
||||
void insert(T value) {
|
||||
EXPECT_TRUE(set.insert(value).second);
|
||||
}
|
||||
|
||||
void check(T value) {
|
||||
EXPECT_TRUE(set.contains(value));
|
||||
}
|
||||
|
||||
void remove(T value) {
|
||||
EXPECT_TRUE(set.erase(value));
|
||||
}
|
||||
};
|
||||
|
||||
class A {
|
||||
Tracker<const A *> *tracker;
|
||||
size_t value;
|
||||
|
||||
explicit A(size_t specialValue) : tracker(nullptr), value(specialValue) {}
|
||||
|
||||
void assignTrackers(const A &other) {
|
||||
if (tracker)
|
||||
tracker->check(this);
|
||||
if (other.tracker)
|
||||
other.tracker->check(&other);
|
||||
if (tracker != other.tracker) {
|
||||
if (tracker)
|
||||
tracker->remove(this);
|
||||
if (other.tracker)
|
||||
other.tracker->insert(this);
|
||||
tracker = other.tracker;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
static A getEmptyKey() {
|
||||
return A(EmptyKey);
|
||||
}
|
||||
static A getTombstoneKey() {
|
||||
return A(TombstoneKey);
|
||||
}
|
||||
|
||||
A(Tracker<const A *> *tracker, size_t value) : tracker(tracker), value(value) {
|
||||
if (tracker)
|
||||
tracker->insert(this);
|
||||
}
|
||||
|
||||
A(const A &other) : tracker(other.tracker), value(other.value) {
|
||||
if (tracker) {
|
||||
tracker->insert(this);
|
||||
tracker->check(&other);
|
||||
}
|
||||
}
|
||||
|
||||
A(A &&other) : tracker(other.tracker), value(other.value) {
|
||||
if (tracker) {
|
||||
tracker->insert(this);
|
||||
tracker->check(&other);
|
||||
}
|
||||
}
|
||||
|
||||
A &operator=(const A &other) {
|
||||
assignTrackers(other);
|
||||
value = other.value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
A &operator=(A &&other) {
|
||||
assignTrackers(other);
|
||||
value = other.value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
~A() {
|
||||
if (tracker)
|
||||
tracker->remove(this);
|
||||
}
|
||||
|
||||
size_t getValue() const {
|
||||
if (tracker)
|
||||
tracker->check(this);
|
||||
return value;
|
||||
}
|
||||
|
||||
friend bool operator==(const A &lhs, const A &rhs) {
|
||||
return lhs.getValue() == rhs.getValue();
|
||||
}
|
||||
};
|
||||
|
||||
static void insertSuccess(SmallMap<A, A> &map, Tracker<const A*> &tracker,
|
||||
size_t key, size_t value) {
|
||||
auto result = map.insert(A(&tracker, key), A(&tracker, value));
|
||||
EXPECT_TRUE(result.second);
|
||||
EXPECT_NE(map.end(), result.first);
|
||||
EXPECT_EQ(value, result.first->getValue());
|
||||
}
|
||||
|
||||
static void insertFailure(SmallMap<A, A> &map, Tracker<const A*> &tracker,
|
||||
size_t key, size_t value, size_t actualValue) {
|
||||
auto result = map.insert(A(&tracker, key), A(&tracker, value));
|
||||
EXPECT_FALSE(result.second);
|
||||
EXPECT_NE(map.end(), result.first);
|
||||
EXPECT_EQ(actualValue, result.first->getValue());
|
||||
}
|
||||
|
||||
static void lookupSuccess(SmallMap<A, A> &map, Tracker<const A*> &tracker,
|
||||
size_t key, size_t value) {
|
||||
auto result = map.find(A(&tracker, key));
|
||||
EXPECT_NE(map.end(), result);
|
||||
EXPECT_EQ(value, result->getValue());
|
||||
}
|
||||
|
||||
static void lookupFailure(SmallMap<A, A> &map, Tracker<const A*> &tracker,
|
||||
size_t key) {
|
||||
auto result = map.find(A(&tracker, key));
|
||||
EXPECT_EQ(map.end(), result);
|
||||
}
|
||||
|
||||
#define INSERT_SUCCESS(KEY, VALUE) \
|
||||
insertSuccess(map, tracker, KEY, VALUE)
|
||||
#define INSERT_FAILURE(KEY, VALUE, ACTUAL) \
|
||||
insertFailure(map, tracker, KEY, VALUE, ACTUAL)
|
||||
#define LOOKUP_SUCCESS(KEY, VALUE) \
|
||||
lookupSuccess(map, tracker, KEY, VALUE)
|
||||
#define LOOKUP_FAILURE(KEY) \
|
||||
lookupFailure(map, tracker, KEY)
|
||||
|
||||
struct entry {
|
||||
size_t key;
|
||||
size_t value;
|
||||
};
|
||||
|
||||
static const entry globalEntries[] = {
|
||||
{ 833286, 244010 },
|
||||
{ 21885, 583865 },
|
||||
{ 98803, 373843 },
|
||||
{ 757849, 280197 },
|
||||
{ 544837, 319456 },
|
||||
{ 301715, 409382 },
|
||||
{ 214164, 173603 },
|
||||
{ 90472, 679461 },
|
||||
{ 454735, 523445 },
|
||||
{ 726077, 442142 },
|
||||
{ 757356, 26085 },
|
||||
{ 83528, 609269 },
|
||||
{ 25506, 528950 },
|
||||
{ 66693, 225472 },
|
||||
{ 850311, 274721 },
|
||||
{ 575211, 385129 },
|
||||
{ 496336, 530893 },
|
||||
{ 753928, 460664 },
|
||||
{ 569603, 263213 },
|
||||
{ 863114, 294890 },
|
||||
{ 289913, 871387 },
|
||||
{ 567663, 970826 },
|
||||
{ 54922, 182147 },
|
||||
{ 234275, 516764 },
|
||||
{ 521608, 771620 },
|
||||
{ 38169, 832007 },
|
||||
{ 777822, 704626 },
|
||||
{ 608984, 769469 },
|
||||
{ 696833, 136927 },
|
||||
{ 336429, 615964 },
|
||||
{ 203555, 147525 },
|
||||
{ 759946, 740892 },
|
||||
{ 702926, 137033 },
|
||||
{ 86701, 400847 },
|
||||
{ 177435, 145944 },
|
||||
{ 424806, 194239 },
|
||||
{ 628673, 279972 },
|
||||
{ 843621, 449262 },
|
||||
{ 372083, 860665 },
|
||||
{ 642760, 534411 },
|
||||
{ 777604, 996069 },
|
||||
{ 942048, 227549 },
|
||||
{ 43009, 551907 },
|
||||
{ 814924, 532395 },
|
||||
{ 480414, 327500 },
|
||||
{ 49853, 745810 },
|
||||
{ 157379, 947358 },
|
||||
{ 313310, 851746 },
|
||||
{ 957411, 179233 },
|
||||
{ 32217, 35134 },
|
||||
{ 684458, 208518 },
|
||||
{ 944720, 998758 },
|
||||
{ 533638, 728837 },
|
||||
{ 670556, 946584 },
|
||||
{ 466090, 456504 },
|
||||
{ 213558, 326747 },
|
||||
{ 967293, 15416 },
|
||||
{ 370014, 356011 },
|
||||
};
|
||||
|
||||
} // end anonymous namespace
|
||||
|
||||
|
||||
TEST(SmallMap, Basic) {
|
||||
Tracker<const A *> tracker;
|
||||
{
|
||||
SmallMap<A, A> map;
|
||||
|
||||
auto entries = llvm::makeArrayRef(globalEntries);
|
||||
|
||||
for (size_t iteration : range(entries.size())) {
|
||||
EXPECT_EQ(iteration, map.size());
|
||||
EXPECT_EQ(iteration == 0, map.empty());
|
||||
|
||||
// Check that previous entries are still there.
|
||||
for (size_t i : range(iteration)) {
|
||||
LOOKUP_SUCCESS(entries[i].key, entries[i].value);
|
||||
INSERT_FAILURE(entries[i].key, MissingValue, entries[i].value);
|
||||
}
|
||||
|
||||
// Check that later entries are not there.
|
||||
for (size_t i : range(iteration, entries.size())) {
|
||||
LOOKUP_FAILURE(entries[i].key);
|
||||
}
|
||||
|
||||
INSERT_SUCCESS(entries[iteration].key, entries[iteration].value);
|
||||
LOOKUP_SUCCESS(entries[iteration].key, entries[iteration].value);
|
||||
}
|
||||
|
||||
EXPECT_EQ(entries.size(), map.size());
|
||||
|
||||
size_t i = 0;
|
||||
for (auto &value : map) {
|
||||
EXPECT_EQ(entries[i].value, value.getValue());
|
||||
i++;
|
||||
}
|
||||
EXPECT_EQ(entries.size(), i);
|
||||
}
|
||||
EXPECT_TRUE(tracker.empty());
|
||||
}
|
||||
|
||||
template <>
|
||||
struct llvm::DenseMapInfo<A> {
|
||||
static inline A getEmptyKey() {
|
||||
return A::getEmptyKey();
|
||||
}
|
||||
static inline A getTombstoneKey() {
|
||||
return A::getTombstoneKey();
|
||||
}
|
||||
static unsigned getHashValue(const A &val) {
|
||||
return val.getValue();
|
||||
}
|
||||
static bool isEqual(const A &lhs, const A &rhs) {
|
||||
return lhs == rhs;
|
||||
}
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user