CS106L-Assignment-2-STL-HashMap

本文介绍了如何在C++中修改CS106LAssignment2中的STLHashMap,包括常函数的使用、at()和[]的区别、特殊成员函数(如构造函数、析构函数、复制和移动操作符)的实现,以及内存管理和效率优化。重点讨论了const-correctness和MoveSemantics在类设计中的应用。
摘要由CSDN通过智能技术生成

title: ‘CS106L Assignment 2: STL HashMap’
date: 2024-02-08 18:30:02
tags: C++
typora-root-url: CS106L-Assignment-2-STL-HashMap

资源请见Assignment 2.

Lecturer Avery Assignment 2 Grading Overview.

Milestone 1: Const-Correstness

  1. 更改main.cppstudent_main()调用函数的参数.
int student_main() {
    cout << "This is student main. You can try using HashMap as a client by editing the code here!" << endl;
    HashMap<string, int> map;
    init_map(map);
    std::set<string> keys = find_keys(map);
    cout << "Find the difference in time between two lecturers! \n" <<
        "Please enter two names from this list, separated by a space. Then hit ENTER\n" << endl;
    for(auto it = keys.begin(); it != keys.end(); ++it){
        cout << *it << endl;
    }
    string name1;
    string name2;
    cin >> name1;
    cin >> name2;
    print_difference(map, name1, name2);
    return 0;
}

其中,init_map需要对参数做改动,不需加const.

void init_map(HashMap<string, int>& map);
void print_difference(const HashMap<string, int>& lecturer_record, const string lecturer1, const string lecturer2);
template<typename KeyType, typename MappedTyped>
const std::set<KeyType>& find_keys(const HashMap<KeyType, MappedTyped>& map);
  1. 更改HashMap类

需要注意const在区分重载函数时也起作用. 对于两个参数列表完全相同的重载函数,只要其一为常函数,那么就是合法的重载. 反之不然.

对于绝大多数不改变对象属性的函数,直接加const即可. 这是因为非常对象亦可调用常函数.

对于at(),需要添加新的重载函数.

这是因为常对象希望返回值为const M&,以保证其值不会通过变量别名的方式修改.

这里为了避免代码重复,我们使用const_cast<newtype>(expression).

注意,只有const_cast<>()可以实现const与nonconst对象之间的转换.

const_cast can be used to remove or add const to a variable; no other C++ cast is capable of removing it (not even reinterpret_cast). It is important to note that modifying a formerly const value is only undefined if the original variable is const; if you use it to take the const off a reference to something that wasn’t declared with const, it is safe. This can be useful when overloading member functions based on const, for instance. It can also be used to add const to an object, such as to call a member function overload.

From stackoverflow.

// ADD
template <typename K, typename M, typename H>
const M& HashMap<K, M, H>::at(const K& key) const{
    // Step 1. transform the const object into nonconst object.
    auto obj = const_cast<HashMap<K, M, H>*>(this);
    // Step 2. call the nonconst at().
    return static_cast<const M&>(obj->at(key));
}
// ENDADD

对于find()等函数,由于其返回值为iterator,因此需要重载.

此处关键词typename告知编译器HashMap<K, M, H>::const_iterator是变量类型. 这样做是因为在模板实例化之前编译器并不知晓其具体定义.

在模板函数的返回值类型依赖于模板参数时,需要使用typename.

// Notice the keyword `typename` for template function.
template <typename K, typename M, typename H>
typename HashMap<K, M, H>::const_iterator HashMap<K, M, H>::find(const K& key) const {
    return make_iterator(find_node(key).second);
}

具体实现如下.

//ADD
template <typename K, typename M, typename H>
typename HashMap<K, M, H>::const_iterator HashMap<K, M, H>::find(const K& key) const {
    HashMap<K, M, H>* obj = const_cast<HashMap<K, M, H>*>(this);
    return static_cast<HashMap<K, M, H>::const_iterator>(obj->find(key));
}
//ENDADD

对于部分函数,文件hashmap.h在注释中已经表示不需考虑.

\\ Hint: on the assignment, you should NOT need to call this function.

Writeup

  1. at() vs []

    Explain the difference between at() and the implementation of the operator []. Wy did you have to overload one and not the other?

    Hint: You will likely only need to read the header comments to do this

    Notes: recall that operator[], which you will implement, does not throw exceptions, if a key is not found. Instead, it will create a K/M pair for that key with a default mapped value. This function is also not const-correct, which you will fix in milestone 2.

    operator[] 函数定义本身允许对Hashmap做改动,因此不应当被常函数调用,不应重载.

  2. Find vs. 𝓕𝓲𝓷𝓭

    In addition to the HashMap::find member function, there is also a std::find function in the STL algorithms library. If you were searching for key k in HashMap m, is it preferable to call m.find(k) or std::find(m.begin(), m.end(), k) ?

    Hint: on average, there are a constant number of elements per bucket. Also, one of these functions has a faster Big-O complexity because one of them uses a loop while another does something smarter.

    find_node()是顺序遍历实现查找的,是较慢的查找算法.

  3. RAII?

    This HashMap class is RAII-compliant. Explain why.

    HashMap的析构函数调用clear(),实现了内存的释放.

  4. Increments

    Briefly explain the implementation of HashMapIterator’s operator++, which we provide for you. How does it work and what checks does it have?

    实现如下.

    template <typename Map, bool IsConst>
    HashMapIterator<Map, IsConst>& HashMapIterator<Map, IsConst>::operator++() {
        _node = _node->next; // _node can't be nullptr - that would be incrementing end()
        if (_node == nullptr) { // if you reach the end of the bucket, find the next bucket
            for (++_bucket; _bucket < _buckets_array->size(); ++_bucket) {
                _node = (*_buckets_array)[_bucket];
                if (_node != nullptr) {
                    return *this;
                }
            }
        }
        return *this;
    }
    
    template <typename Map, bool IsConst>
    HashMapIterator<Map, IsConst> HashMapIterator<Map, IsConst>::operator++(int) {
        auto copy = *this; // calls the copy constructor to create copy
        ++(*this);
        return copy;
    }
    

    这里重载是为了区分++VarVar++. 这样的区分是如何实现的?

    To solve this problem, the postfix versions take an extra (unused) parameter of type int. When we use a postfix operator, the compiler supplies 0 as the argument for this parameter. Although the postfix function can use this extra parameter, it usually should not. That parameter is not needed for the work normally performed by a postfix operator. Its sole purpose is to distinguish a postfix function from the prefix version.

    From C++ Primer.

Milestone 2: Special Member Functions and Move Semantics

为 HashMap 实现 SMF.

  • Default constructor (implemented for you)
  • Destructor (implemented for you)
  • Copy constructor
  • Copy assignment operator
  • Move constructor
  • Move assignment operator

Copying is not as simple as copying each member variable, that’s why we need SMF.

Copy Constructor/Assignment Operator

需要使用insert().

template<typename K, typename M, typename H>
HashMap<K, M, H>::HashMap(const HashMap& other){
    for (auto [key, value] : other) {
        this->insert(std::make_pair(key, value));
    }
    return *this;
}

template<typename K, typename M, typename H>
HashMap<K, M, H>& HashMap<K, M, H>::operator=(const HashMap& other){
    if (*this == other) return *this;// avoid self-reassignment.
    clear();
    for (auto [key, value] : other) {
        this->insert(std::make_pair(key, value));
    }
    return *this;
}

Move Constructor/Assignment Operator

构造函数利用参数列表传参时有两种写法.

template<typename K, typename M, typename H>
HashMap<K, M, H>::HashMap(const HashMap&& other) : 
    _size{std::move(other._size)},
    _hash_function{std::move(other._hash_function)},
    _buckets_array{other.bucket_count(), nullptr} {
        /*code*/
}

其中{}可替换成(),使用花括弧的好处如下.

The {} syntax, known as brace or list initialization, was introduced in C++11 and has some advantages over ():

  • It disallows narrowing conversions (e.g., from double to int).
  • It can be used to initialize aggregate types.
  • It can be used to call a constructor that takes an std::initializer_list.

注意在实现时不要将右值引用作为常参数,因为我们需要对其做如下类似改动.

other._elems = nullptr;
other._size = 0;
HashMap<K, M, H>::HashMap(HashMap&& other) :
    _size{std::move(other._size)},
    _hash_function{std::move(other._hash_function)},
    _buckets_array{other.bucket_count(), nullptr}
{
    _buckets_array.resize(other.bucket_count());
    for(size_t i = 0; i < other.bucket_count(); i++){
        _buckets_array[i] = std::move(other._buckets_array[i]);
        other._buckets_array[i] = nullptr;
    }
    other._size = 0;
}

template<typename K, typename M, typename H>
HashMap<K, M, H>& HashMap<K, M, H>::operator=(HashMap&& other){
    if (*this == other) return *this;// avoid self-reassignment.
    clear();

    _size = std::move(other._size);
    _hash_function = std::move(other._hash_function);
    _buckets_array.resize(other.bucket_count());
    for(size_t i = 0; i < other.bucket_count(); i++){
        _buckets_array[i] = std::move(other._buckets_array[i]);
        other._buckets_array[i] = nullptr;
    }
    other._size = 0;
    return *this;
}

Test

运行测试无输出,既不显示结果,亦不输出错误.

与正确答案做对比.

替换tests.cpptest_setting.cpp,运行正常.

替换main.cpphashmap_iterator.cpp,运行正常.

找了两三天😓…逐行对比,发现错误.

//ADD
template <typename K, typename M, typename H>
typename  HashMap<K, M, H>::const_iterator HashMap<K, M, H>::end() const{
    //return static_cast<HashMap<K, M, H>::const_iterator>(this->end());
    return static_cast<const_iterator>(const_cast<HashMap<K, M, H>*>(this)->end());
}
//ENDADD

结果如下.

E:\CS106L\assignments\HashMap_Starter>.\main
Please enter a number and then hit ENTER
1) Milestone 1 code
2) Milestone 2 tests
2
:P

----- CS 106L SIMPLE TEST HARNESS -----
Written by Avery Wang (2019-2020 lecturer)


----- Milestone 2 Tests -----
Test A_copy_ctor_basic              PASS
Test B_copy_assign_basic            PASS
Test C_copy_edge                    PASS
Test D_move_ctor_basic              PASS
Test E_move_assign_basic            PASS
Test F_move_edge                    PASS
Move    10 elements: 0 ns
Move   100 elements: 0 ns
Move  1000 elements: 0 ns
Move 10000 elements: 0 ns
Test G_move_ctor_time               FAIL: VERIFY_TRUE assertion failure at line number 1681 in file tests.cpp
Move    10 elements: 0 ns
Move   100 elements: 0 ns
Move  1000 elements: 0 ns
Move 10000 elements: 0 ns
Test H_move_assign_time             FAIL: VERIFY_TRUE assertion failure at line number 1748 in file tests.cpp

----- Test Harness Summary -----
Required Tests passed: 6/8
Some required tests were failed or skipped.

----- End of Test Harness -----

Writeup

  1. Attachment Issues

    Why is it that we need to implement the special member functions in the HashMap class, but we can default all the special member functions in the HashMapIterator class?

    Hint: your answer should reference the Rule of Five (the Rule of 3 including move operations) vs. the Rule of Zero, and also talk about std::vector’s copy constructor/assignment operator.

    主要涉及到深拷贝.

  2. Move Semantics

    In your move constructor or move assignment operator, why did you have to std::move each member, even though the parameter (named rhs) is already an r-value reference?

    以下回答来自Copilot.

    The std::move is used in move constructors or move assignment operators to indicate that an object t may be “moved from”, i.e., allowing the efficient transfer of resources from t to another object.

    In your case, even though the parameter rhs is already an r-value reference, you still need to use std::move on each member because rhs is a named r-value reference. Once an r-value reference has been named (like rhs in your case), it becomes an l-value and cannot be moved from without std::move.

    Here’s a simple example to illustrate this:

    class MyClass {
        std::vector<int> vec;
    public:
        MyClass(std::vector<int>&& vec) : vec(vec) {} // Incorrect
        MyClass(std::vector<int>&& vec) : vec(std::move(vec)) {} // Correct
    };
    

    In the incorrect constructor, vec is a named r-value reference, so it’s treated as an l-value and the copy constructor of std::vector is called, which is not what we want. In the correct constructor, std::move is used to cast the named r-value reference back to an r-value, which calls the move constructor of std::vector, achieving the desired efficiency.

    Slides中有讲解如下.

在这里插入图片描述

实现HashMap

暂时结束. 早年的作业似乎是徒手实现这样一个hashmap,现在作业的难度大大降低. 数据结构学完之后,或可尝试实现.

  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值