Tuesday, March 11, 2025

rule of 3 5 6 in C++

 

Rule of 3 (C++98/03)

In C++98/03, the Rule of 3 states that if a class defines one of the following special member functions, it should likely define all three:

  1. Destructor: Cleans up any dynamically allocated memory or other resources.
  2. Copy Constructor: Creates a new object as a copy of an existing object.
  3. Copy Assignment Operator: Assigns the contents of one object to another.

Rule of 5 (C++11 and beyond)

With the introduction of C++11, the Rule of 5 builds upon the Rule of 3 and adds two more special member functions:

  1. Move Constructor: Transfers resources from one object to another without copying.
  2. Move Assignment Operator: Transfers resources from one object to another using move semantics rather than copying.

Rule of 6 (Extended rule, but not part of the C++ standard)

In some discussions, you may encounter references to the Rule of 6, which is an extension of the Rule of 5. It specifically addresses the issue of defining default constructor alongside the other special functions.

The Rule of 6 says that if you define any of the following special functions, you should define all six of them:

  1. Destructor
  2. Copy Constructor
  3. Copy Assignment Operator
  4. Move Constructor
  5. Move Assignment Operator
  6. Default Constructor

This rule helps to ensure that all necessary behaviors are correctly defined for managing resource ownership and memory management. In practice, it's more of a guideline to help developers avoid incomplete implementations when defining complex classes with custom resource management.

what happens when unordered_map keeps increasing in size?

 When a std::unordered_map keeps increasing in size, it dynamically manages its internal storage by resizing and rehashing the underlying hash table. This process helps to maintain efficient average-case performance for operations like insertions, lookups, and deletions. Here's what happens in more detail as the container grows:

1. Load Factor and Rehashing

The load factor is a key concept when it comes to how an unordered_map handles growth. It is defined as the ratio of the number of elements (n) in the container to the number of buckets (b) in the hash table:

bash
load factor = n / b

As the unordered_map grows, the load factor increases. If it exceeds a certain threshold (typically around 1.0), the map will rehash to maintain efficient performance. This threshold is dependent on the implementation, but most implementations will rehash when the load factor exceeds 1.0 (i.e., when the number of elements exceeds the number of buckets).

2. Resizing and Rehashing Process

When the unordered_map reaches its threshold for the load factor (or exceeds it), it will trigger a rehash. Rehashing involves:

  • Increasing the number of buckets: The number of buckets is typically doubled (or increased by some other factor), which reduces the load factor and helps distribute elements more evenly across the new buckets.
  • Rehashing the existing elements: All the elements in the current hash table are rehashed and redistributed across the new set of buckets. The elements are hashed again using the container's hash function.

3. Impact of Rehashing

Rehashing can have the following impacts:

  • Time Complexity for Rehashing: Rehashing itself is an O(n) operation, where n is the number of elements in the unordered_map because all existing elements must be rehashed and placed in the new buckets. This means that rehashing is relatively expensive when the map grows.

  • Performance During Rehashing: Even though rehashing is expensive, insertions and lookups after rehashing are still expected to be O(1) on average, assuming good hash distribution. However, during the rehashing process, the time taken for insertion will increase temporarily.

  • Memory Usage: As the number of buckets increases, so does the memory usage. While the load factor decreases, the total memory used by the unordered_map increases due to the larger hash table, which may lead to higher memory consumption than the actual number of elements.

4. Overhead of Large unordered_map

If the map grows excessively large, there are some issues to consider:

  • Memory Consumption: The memory overhead can become significant because the hash table must maintain extra space for each bucket, even though some buckets may remain empty. In extreme cases, the size of the hash table could become much larger than the actual number of elements stored.

  • Frequent Rehashing: If the map keeps growing rapidly and the load factor remains high, it can lead to frequent rehashing as the map tries to keep the load factor low. This can significantly impact performance, especially if the number of insertions is high.

  • Fragmentation: As the unordered_map resizes and reallocates its internal storage, memory fragmentation might occur, especially in cases where the size of the map grows rapidly and often.

5. Controlling Growth and Resizing

To optimize the performance and memory usage of a growing unordered_map, you can control its growth manually by using the following functions:

  • reserve(size_t n): This function allows you to pre-allocate space for at least n elements. If you know that the unordered_map will grow to a large size, you can reserve enough space upfront to avoid frequent rehashing. This helps reduce the overhead caused by resizing the hash table as the container grows.

    cpp
    std::unordered_map<int, int> map; map.reserve(1000); // Reserve space for at least 1000 elements
  • rehash(size_t n): This function allows you to set the number of buckets to at least n, forcing the unordered_map to rehash and redistribute the elements across a specific number of buckets. This can be useful if you expect the map to hold a certain number of elements and want to control its size in advance.

    cpp
    map.rehash(2000); // Set the number of buckets to 2000

6. Example: Growth and Resizing in Action

Let's look at a simple example to see how the unordered_map grows.

cpp
#include <iostream> #include <unordered_map> int main() { std::unordered_map<int, int> map; // Initially, unordered_map starts with a small number of buckets std::cout << "Initial bucket count: " << map.bucket_count() << std::endl; for (int i = 0; i < 100; ++i) { map[i] = i; // Insert elements if (map.bucket_count() != map.max_bucket_count()) { std::cout << "Bucket count increased to: " << map.bucket_count() << std::endl; } } std::cout << "Final bucket count: " << map.bucket_count() << std::endl; std::cout << "Map size: " << map.size() << std::endl; return 0; }

This example will show how the number of buckets changes as more elements are inserted. Typically, you'll see that as the number of elements exceeds the initial number of buckets, the unordered_map will double the number of buckets and rehash its contents.

Summary of What Happens During Growth:

  • Rehashing occurs when the load factor exceeds the threshold (typically 1.0).
  • The number of buckets increases (often doubling), which helps reduce the load factor.
  • Memory usage increases because the number of buckets grows, and the elements are redistributed across the new buckets.
  • Insertion and lookup operations remain O(1) on average, but rehashing itself is an expensive O(n) operation.
  • You can use reserve() and rehash() to control growth and reduce the impact of resizing.

While unordered_map is efficient for average-case lookups and insertions, you need to be mindful of its growth behavior and control it when dealing with large datasets or high-performance requirements.

std::unordered_map problems

 The std::unordered_map in C++ is a powerful container that provides fast access to key-value pairs with average constant-time complexity (O(1)) for lookups, inserts, and deletions. However, like any data structure, it has its potential issues, trade-offs, and limitations. Here are some common problems and limitations associated with std::unordered_map:

1. Unpredictable Iteration Order

  • Problem: The order of elements in an unordered_map is not guaranteed. It depends on the internal hash table implementation and the distribution of hash values.
  • Impact: This makes std::unordered_map unsuitable for use cases where the order of elements matters, such as when you need to traverse the elements in a specific order.
  • Example:
    cpp
    std::unordered_map<int, std::string> map; map[1] = "one"; map[2] = "two"; map[3] = "three"; // The order in which elements are iterated is not guaranteed for (auto& entry : map) { std::cout << entry.first << ": " << entry.second << std::endl; }

2. Inefficient for Small Number of Elements

  • Problem: std::unordered_map may not be efficient for containers with a small number of elements, as it incurs overhead due to the use of a hash table.
  • Impact: For small datasets, a std::map (which is a balanced binary search tree) might offer better performance since it has lower constant overhead.
  • Example: If you are working with only a few elements and there’s no need for fast lookups, using an ordered std::map could be simpler and more efficient.

3. Hash Collisions

  • Problem: The performance of std::unordered_map can degrade significantly if there are a lot of hash collisions. Collisions occur when different keys produce the same hash value, causing the elements to be stored in the same bucket.
  • Impact: Collisions can cause the time complexity of lookups, inserts, and deletions to degrade from O(1) to O(n) in the worst case, where n is the number of elements in the bucket.
  • Solution: A good custom hash function (if needed) and resizing the hash table (using rehash() or reserve()) to ensure the load factor remains low can mitigate this problem.

4. Unpredictable Memory Usage

  • Problem: std::unordered_map can use more memory than expected due to the internal hash table. The memory overhead depends on the number of buckets and how well the hash function distributes keys across those buckets.
  • Impact: If not carefully managed, an unordered_map can consume more memory than necessary, especially when there are many buckets but few elements. This overhead can become significant when storing large numbers of elements.
  • Solution: You can use the rehash() function to control the number of buckets in the hash table or reserve() to allocate space upfront to reduce unnecessary rehashing.

5. Hash Function Dependency

  • Problem: The performance of std::unordered_map heavily depends on the quality of the hash function used. A poor hash function can lead to frequent collisions and inefficient lookups.
  • Impact: A poor hash function might significantly reduce the efficiency of the container, causing the average lookup time to become much worse than O(1).
  • Solution: If you're using custom types as keys, ensure you implement a good hash function. The C++ Standard Library provides std::hash for standard types, but for custom types, you might need to write your own hash function.
    cpp
    struct MyKey { int a, b; }; struct MyKeyHash { std::size_t operator()(const MyKey& key) const { return std::hash<int>()(key.a) ^ std::hash<int>()(key.b); } }; std::unordered_map<MyKey, std::string, MyKeyHash> my_map;

6. Resize/Resize Performance

  • Problem: std::unordered_map may reallocate and resize its internal hash table as elements are added, which can result in performance degradation if the container grows significantly and frequently. Resizing a hash table is an expensive operation.
  • Impact: If the container grows quickly, you may experience poor performance due to the frequent resizing and rehashing.
  • Solution: Pre-allocate the desired space using reserve() to avoid frequent resizing during insertions.
    cpp
    std::unordered_map<int, std::string> map; map.reserve(1000); // Reserve space for 1000 elements

7. Lack of Thread Safety

  • Problem: std::unordered_map is not thread-safe. If multiple threads are concurrently modifying or reading from the same unordered_map, it can lead to data corruption or undefined behavior.
  • Impact: If your application requires concurrent access, you'll need to protect the unordered_map with synchronization mechanisms like std::mutex or use other thread-safe containers.
  • Solution: Use a std::mutex or other synchronization techniques to prevent data races. Alternatively, consider using concurrent data structures, such as those available in Intel Threading Building Blocks (TBB) or concurrent_unordered_map in C++17.

8. Iterator Invalidation

  • Problem: Inserting or erasing elements in std::unordered_map can invalidate iterators. This is especially important when iterating over the container while performing operations.
  • Impact: If you hold an iterator to an element while modifying the container, it may become invalid, leading to undefined behavior.
  • Solution: Be cautious when modifying the container during iteration. If possible, avoid inserting or deleting while iterating or use iterators that are guaranteed to remain valid, such as iterators returned by erase().
    cpp
    for (auto it = map.begin(); it != map.end(); ) { if (some_condition) { it = map.erase(it); // Erase and get the next valid iterator } else { ++it; } }

9. Complexity of Custom Key Types

  • Problem: If you are using custom types as keys in std::unordered_map, you need to ensure that the types are hashable and comparable. If you don't provide a good hash function and == operator, performance may suffer, or the map may not work as expected.
  • Impact: If the custom key type is poorly implemented or doesn’t provide a proper hash function, it can lead to incorrect behavior or significant performance degradation.
  • Solution: Always provide a robust hash function and equality operator (operator==) for custom types used as keys.

10. C++11 and Later Features

  • Problem: In C++11 and later versions, the behavior of std::unordered_map changed with respect to hash functions and bucket allocation. The default hash function might not work well for all custom types, and iterators may not remain stable when the underlying container is rehashed.
  • Impact: You need to be aware of the changes introduced in newer C++ standards and ensure that your custom types have compatible hash functions and handle iterator invalidation carefully.

Conclusion

While std::unordered_map is generally a highly efficient container, it is important to understand its limitations and when it might not be the best choice. By being mindful of issues like hash collisions, iterator invalidation, memory usage, and thread safety, you can ensure that you're using std::unordered_map effectively in your C++ code. When in doubt, consider alternative containers such as std::map (for ordered data), or specialized concurrent containers when working with multi-threaded code.

C++11 faster than C++03

 C++11 faster than C++03 : Why and How



C++11 introduced several features and improvements that can make code written in C++11 faster than code written in C++03. Here are some key reasons:

1. Move Semantics

  • C++11 introduced move semantics, which allow the resources of temporary objects to be "moved" rather than copied. This reduces unnecessary deep copies of objects, especially for classes that manage resources like dynamic memory, file handles, or sockets. This can lead to significant performance improvements in applications that create and destroy many temporary objects.

2. Rvalue References

  • Move semantics are enabled by rvalue references (denoted by &&), which allow developers to distinguish between objects that can be moved (temporaries) and those that cannot. This optimization minimizes the overhead of copying large objects.

3. Improved Concurrency Support

  • C++11 introduced a standardized memory model and threading library (<thread>, <mutex>, <future>, <atomic>, etc.). This allows for more efficient and portable multithreaded code. The memory model also helps in writing lock-free and wait-free algorithms, which can improve performance in multi-core systems.

4. Optimized Standard Library

  • The Standard Template Library (STL) was updated in C++11 to take advantage of new language features like move semantics and improved algorithms. For example, many standard containers (e.g., std::vector, std::string) benefit from move operations, reducing overhead in common operations like reallocation or sorting.

5. constexpr

  • The constexpr keyword in C++11 allows the evaluation of functions and expressions at compile time rather than runtime. This can lead to significant performance gains by reducing runtime computation and improving opportunities for compiler optimizations.

6. Uniform Initialization

  • C++11 introduced uniform initialization (using curly braces {}), which can lead to more efficient code generation by the compiler. It also helps avoid certain pitfalls associated with older initialization syntax.

7. auto and type inference

  • The auto keyword allows the compiler to infer the type of a variable. While this doesn't directly improve performance, it can enable better optimizations by the compiler, especially when used with template-heavy code.

8. Range-based for Loop

  • The range-based for loop (for (auto &x : container)) introduced in C++11 can make iteration over containers more efficient by eliminating the need for manual indexing or iterator management.

9. nullptr

  • The introduction of nullptr in C++11 provides a type-safe null pointer constant, which can lead to better optimizations and prevent bugs that might otherwise lead to inefficiencies in the code.

10. Lambda Expressions

  • Lambda expressions provide a concise way to define anonymous functions directly within code, which can lead to better optimization opportunities by the compiler, especially when used in algorithms.

11. New Containers and Smart Pointers

  • C++11 added new containers like std::array (a stack-allocated array) and smart pointers (std::unique_ptr, std::shared_ptr), which can help manage resources more efficiently and avoid performance issues related to manual memory management.

12. Improved Compiler Optimizations

  • The introduction of these features and the move towards modern C++ coding practices have also led to better compiler optimizations. Modern C++ compilers are designed to take full advantage of the language features introduced in C++11, leading to faster and more optimized code compared to C++03.

Overall, the combination of these features and improvements allows C++11 code to be more efficient and often faster than equivalent C++03 code.

my bashrc

 #Source global defs

if [ -f /etc/bashrc ]; then

. /etc/bashrc

fi


if [ -e “$HOME”/.bash_aliases ]; then

source “$HOME”/.bash_aliases

fi


#User specific env

if ! [[ “$PATH” =~”$HOME/.local/bin:$HOME/bin:” ]]

then

PATH=“$HOME/.local/bin:$HOME/bin:$PATH”

fi

export PATH


ORACLE_HOME=/opt/oracle

export ORACLE_HOME

ORACLE_SID=OTP01

export ORACLE_SID

JAVA_HOME=/usr/lib/jvm/java-1.8.0

export JAVA_HOME



alias ll=‘ls -l | more’

alias lt=‘ls -t | more’

alias lsa=‘ls -a | more’

alias lla=‘ls -la | more’

alias lR=‘ls -R | more’

alias ltr=‘ls -ltr | more’

alias ldir=‘ls -l | grep ^d | more’

alias c=‘clear’

alias h=‘history 200’

alias find=‘find 2>/dev/null’ #what does 2>/dev/null does?

alias sq=‘sqlplus username/password@schemaname’

alias srcrc=‘source ~/.bashrc’

alias myps=‘ps -aef | grep dagaa | grep -v vscode | more’ #displays processes for dagaa except for vscode

alias myps=‘ps -aef | grep dagaa | grep vscode | more’ #displays vscode processes for dagaa 

alias gs=‘git status’

alias gb=‘git branch’

alias gl=‘git log’

alias gd=‘git diff’

alias run_send_to_mnc=‘cdmnc && python send_to_mnc.py http://dc2dvxdsklv19.ipa.lmeft.co.uk:38501’

google test for a code that has random number generator

 Writing tests for code that uses a random number generator (RNG) can be tricky, as random behavior introduces non-determinism into your tests. However, you can make testing easier by controlling or "mocking" the randomness. There are several strategies you can use to test code that involves random numbers, particularly when using a testing framework like Google Test.

Here are the steps to test code that uses a random number generator:

1. Refactor to Allow Injecting the RNG

If possible, refactor your code to allow the random number generator to be injected (dependency injection). This allows you to substitute the random number generator with a mock or predictable RNG during testing.

Example Code to Refactor:

cpp
#include <iostream> #include <random> class RandomGenerator { public: virtual int generate() = 0; }; class RealRandomGenerator : public RandomGenerator { public: int generate() override { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(1, 100); return dis(gen); } }; class MyClass { private: RandomGenerator* rng; public: MyClass(RandomGenerator* rng) : rng(rng) {} int getRandomValue() { return rng->generate(); } };

In this refactored code, RandomGenerator is an abstract class that is implemented by RealRandomGenerator. The MyClass class takes a pointer to a RandomGenerator in its constructor, allowing you to inject different implementations for testing purposes.

2. Write Google Test for Injected RNG

Now, you can write a test that uses a mock RNG. In Google Test, you can use Google Mock to create a mock class for RandomGenerator that returns predictable values.

Step-by-Step Google Test Example:

cpp
#include <gtest/gtest.h> #include <gmock/gmock.h> // Mock class for RandomGenerator class MockRandomGenerator : public RandomGenerator { public: MOCK_METHOD(int, generate, (), (override)); }; // Test for MyClass TEST(MyClassTest, GetRandomValue) { MockRandomGenerator mockRng; // Set up the mock to return predictable values EXPECT_CALL(mockRng, generate()) .WillOnce(testing::Return(42)) // First call will return 42 .WillOnce(testing::Return(99)); // Second call will return 99 MyClass obj(&mockRng); // Test that the first call returns 42 EXPECT_EQ(obj.getRandomValue(), 42); // Test that the second call returns 99 EXPECT_EQ(obj.getRandomValue(), 99); }

Key Elements of the Test:

  1. MockRandomGenerator: The mock class simulates the behavior of the RandomGenerator. We use Google Mock's MOCK_METHOD macro to create the mock method generate().

  2. EXPECT_CALL: We use EXPECT_CALL to specify that the generate() method should be called, and we use WillOnce(testing::Return(value)) to define what value it should return when called.

  3. Testing: After injecting the mock into the class, we test the behavior of the MyClass methods. We verify that the correct value is returned when the random number is generated, allowing us to validate the logic without involving true randomness.

3. Test with Real RNG (Optional)

If you want to test the real random number generator as well, you can do so by controlling the seed or by using a known seed value in the random generator.

cpp
TEST(MyClassTest, RealRandomValue) { RealRandomGenerator realRng; MyClass obj(&realRng); // Since random numbers are unpredictable, we can't check the exact number, // but we can test if the value falls within a certain range. int value = obj.getRandomValue(); EXPECT_GE(value, 1); EXPECT_LE(value, 100); }

In this test, we’re not testing for a specific random number (since it's truly random), but we can verify that the generated value falls within the expected range (1 to 100 in this case).

4. Use a Fixed Seed for Predictability

If you want to make the tests more deterministic but still use a real RNG, you can fix the random number generator's seed to a known value. This way, the sequence of random numbers generated will always be the same across runs.

cpp
class FixedSeedRandomGenerator : public RandomGenerator { public: int generate() override { static std::mt19937 gen(12345); // Fixed seed for predictability std::uniform_int_distribution<> dis(1, 100); return dis(gen); } }; TEST(MyClassTest, FixedSeedRandomValue) { FixedSeedRandomGenerator fixedRng; MyClass obj(&fixedRng); // Now the sequence of numbers generated is fixed, so we can test it deterministically. EXPECT_EQ(obj.getRandomValue(), 25); // Fixed result from fixed seed }

5. Testing Edge Cases

You can test edge cases like generating the minimum or maximum possible random values:

cpp
TEST(MyClassTest, EdgeCaseRandomValue) { MockRandomGenerator mockRng; EXPECT_CALL(mockRng, generate()) .WillOnce(testing::Return(1)); // Min value EXPECT_CALL(mockRng, generate()) .WillOnce(testing::Return(100)); // Max value MyClass obj(&mockRng); // Test the min value EXPECT_EQ(obj.getRandomValue(), 1); // Test the max value EXPECT_EQ(obj.getRandomValue(), 100); }

Summary:

To effectively test code that involves randomness:

  • Refactor your code to allow dependency injection of the RNG.
  • Use Google Mock to mock or control the behavior of the RNG.
  • You can use a fixed seed for reproducible tests.
  • Validate that your code behaves correctly even with predictable/random values within expected ranges.

By following these strategies, you can create reliable and deterministic tests for code that involves random number generation.