: title : PB161 Programming in C++ : authors : Petr Ročkai : doctype : lnotes : typing : plain # A. Preliminaries This document is a collection of exercises and commented source code examples. All the sources are also available as separate files that you can edit and compile (we will refer to these files as the «source bundle»). Additionally, this section contains the rules and general guidelines that apply to the course as a whole. The latest version of this document along with the source bundle is available both in the study materials in IS and on the student server ‹aisa›: • ‹https://is.muni.cz/auth/el/fi/jaro2022/PB161/um/› – a PDF in ‹pb161.seminar.pdf› and the source bundle in directories ‹01› through ‹12›, ‹t1› through ‹t3› and ‹sol› – use the ‘download as ZIP’ option in the sidebar to get entire directories in one go, • log into ‹aisa› using ‹ssh› or ‹putty›, run ‹pb161 update›, then look under ‹~/pb161› (this chapter is in subdirectory ‹00›). We will update the files as needed, to correct mistakes or include additional material. On ‹aisa›, running ‹pb161 update› at any time will update your working copies, taking care not to overwrite your changes. It will also tell you which files have been updated. ## Course Overview Welcome to PB161 Programming in C++. The course consists of lectures, weekly seminars, programming tasks, and a programming test (exam) at the end. Since this is a programming subject, most of the coursework – and grading – will center around actual programming. You will write a few tiny programs (15-20 minutes each) every week, a few bigger programs (though still small, at a couple hundred lines each) during the semester and there will be a simple (but strict) programming test at the end (in the exam period) that you have to pass. Writing programs is «hard» and consequently, this course will also be hard – you absolutely need to put in effort to pass the subject. Hopefully, you will have learned something by the end of it. Further details on the organisation of this course are in this directory or, if you are reading the PDF, in the following sections: • ‹2_grading.txt› – what is graded and how; what you need to pass, • ‹3_tasks.txt› – general guidelines that govern assignments, • ‹4_reviews.txt› – writing and receiving peer reviews, • ‹5_quality.txt› – code quality guidelines, • ‹6_exam.txt› – about the final programming exam, • ‹7_plagiarism.txt› – about cheating. ### Topics The semester is organized as three four-week blocks. Each week corresponds to a single chapter in this document, for a total of 12 chapters. The study materials for each week are in directories ‹01› through ‹12› (one per week). Start by reading the introduction (‹00_intro.txt› in the ‘source’ version). Each block is followed by a set of bigger tasks, in directories ‹t1› through ‹t3›. Again, start by reading the introduction (‹00_intro.txt›) in there. │ block │ │ topic │ lect. │ prep │ end │ ├───────│─────▻┼◅──────────────────────────│───────│───────│───────│ │ 1 │ 1. │ semantics 1, classes, … │ 15.2. │ 19.2. │ │ │ │ 2. │ semantics 2, lambdas, … │ 22.2. │ 26.2. │ │ │ │ 3. │ containers, algorithms │ 1.3. │ 5.3. │ │ │ │ 4. │ overloading, types, … │ 8.3. │ 12.3. │ 19.3. │ │┄┄┄┄┄┄┄│┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ 2 │ 5. │ operators, IO │ 15.3. │ 19.3. │ │ │ │ 6. │ RAII & exceptions │ 22.3. │ 26.3. │ │ │ │ 7. │ memory, ‹unique_ptr› │ 29.3. │ 2.4. │ │ │ │ 8. │ OOP │ 5.4. │ 9.4. │ 16.4. │ │┄┄┄┄┄┄┄│┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ 3 │ 9. │ templates 1 │ 12.4. │ 16.4. │ │ │ │ 10. │ templates 2 │ 19.4. │ 23.4. │ │ │ │ 11. │ iterators │ 26.4. │ 30.4. │ │ │ │ 12. │ review │ 3.5. │ 7.5. │ 14.5. │ │┄┄┄┄┄┄┄│┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ – │ 13. │ C++20 │ 10.5. │ – │ │ ## Grading Overview There is a number of ways to obtain points: │ max │ what │ notes │ ├─────▻│◅────────────│◅─────────────────────────────────────────│ │ 36pt │ tasks │ 3pt each, max 12 │ │ 12pt │ code review │ 2pt each, max 6 │ │ 12pt │ seminars │ 1pt/week: prep exercises + attendance │ │ 6pt │ sets │ 2pt extra for each set with 4+ points │ │ 3pt │ peer review │ 0.3pt per review, 10 reviews max │ │ 3pt │ activity │ 2pt + 1pt │ │ 18pt │ exam │ 3pt + 4pt + 5pt + 6pt │ │┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│ │ 90pt │ total │ 72pt semester + 18pt exam │ The semester maximum is «72 point». You need «40 points» to pass the semester (failing to do this means grade X). To pass the exam, you need «8 points» on the exam. The final grade is then awarded as follows: • [68, 90] points → A • [63, 68) points → B • [58, 63) points → C • [53, 58) points → D • [48, 53) points → E • [40, 72] points → Z (if your ending type is ‘z’). ### Seminars The preparatory exercises are to be worked out in the corresponding week of the semester, with a deadline every Saturday at midnight (see the semester overview in the previous section, column ‘prep’ for exact dates). Only the enclosed tests are executed upon submission, and the result should appear in the corresponding notepad within 5-10 minutes. Together with seminar attendance, these exercises are worth a significant fraction of what you need to pass the semester. You are awarded a point for each unit (week) in which you submit at least 3 preparatory exercises and attend¹ the corresponding seminar in the following week. In addition to gaining points, you may also get feedback and a chance to discuss the submitted solutions in the seminar. The activity points are likewise awarded in the seminar, in this case for demonstrating the solution of one of the r-type exercises for the week. These will be ‘live’ demos: you should solve the exercise on the spot, without looking at a prepared solution (whether your own or the reference one). You can do them from your own computer (using ‹pb161 beamer› on ‹aisa›) or you can go to the front and use the teacher's computer. In any case, in addition to writing down a solution, you will be expected to comment what and why you are doing. On the other hand, the teacher will help you out if you get stuck in a blind alley. You can do this twice during the semester: the first instance counts as 2 points, the second as 1. If nobody else is interested, you can also volunteer to do an exercise ‘for free’ (that is, if you already have your 3 activity points). ¹ If you cannot attend the seminar and file the requisite paperwork excusing you from attendance, you can instead show «original» solutions for 3 of the r-type exercises from the affected unit to your seminar tutor. If they are satisfactory, you will get a point as if you have attended the seminar. Especially in case of an extended illness, you can also ask for an extension of affected prep exercise submission deadlines. Be sure to do this as soon as you reasonably can. ### Programming Tasks In each block, there are 4 tasks of increasing difficulty (both within and between blocks). There will be 8 deadlines for each «block», spread out over 6 weeks (there's a deadline once a week for the first month, then twice a week for another 2 weeks). Most deadlines are on Saturday (same as weekly exercises), the 2 extras are on Wednesdays. Submissions open 7 days before the first verity deadline (that is 19.2., 26.3. and 23.4. – submissions done before these dates will not be evaluated). Each deadline gives you one chance to pass the automated test suite. It does not matter when you pass any given task, but the test suite is strictly binary: you either pass or you fail. More details and guidelines are in ‹3_tasks.txt›. Verity tests continue to run after the last deadline: you can finish tasks and still get results after they expire, but you will not get any points for doing so. However, it does unlock peer reviews for the given task. The deadline schedule is as follows: │ week │ set 1 │ set 2 │ set 3 │ ├┄┄┄┄┄┄│┄┄┄┄┄┄▻┼┄┄┄┄┄┄▻│┄┄┄┄┄┄▻┼┄┄┄┄┄┄▻│┄┄┄┄┄┄▻┼┄┄┄┄┄┄▻│ │ │ Wed │ Sat │ Wed │ Sat │ Wed │ Sat │ │ 1 │ │ 26.2. │ │ │ │ │ │ 2 │ │ 5.3. │ │ │ │ │ │ 3 │ │ 12.3. │ │ │ │ │ │ 4 │ │ 19.3. │ │ │ │ │ │ 5 │ 23.3. │ 26.3. │ │ 26.3. │ │ │ │ 6 │ 30.3. │ 2.4. │ │ 2.4. │ │ │ │ 7 │ │ │ │ 9.4. │ │ │ │ 8 │ │ │ │ 16.4. │ │ │ │ 9 │ │ │ 20.4. │ 23.4. │ │ 23.4. │ │ 10 │ │ │ 27.4. │ 30.4. │ │ 30.4. │ │ 11 │ │ │ │ │ │ 7.5. │ │ 12 │ │ │ │ │ │ 14.5. │ │ 13 │ │ │ │ │ 18.5. │ 21.5. │ │ 14 │ │ │ │ │ 25.5. │ 28.5. │ ### Code Quality We should all strive to always write clean, readable and well-designed code. Of course, this takes more time (often a lot more time) than just going with the first thing that sort of works. You will be able to submit «six» of your task solutions for teacher review. Which assignments you choose to submit is up to you. Make sure that you put in adequate effort to make the code as clean and nice as you possibly can. The code must pass verity tests (within the designated deadlines). The last day on which you can file a request for (teacher) review is «3.4.» for T.1, «1.5.» for T.2 and «29.5.» for T.3 (i.e. the day after the last test deadline). Your teacher will have 10 days to complete the review. • reviewer deadline: request + 10 days (i.e. 13.4., 25.5. and 8.6.) • resubmit until 17.4., 15.5. and 12.6. • earlier request → more time to fix stuff With each review, you get a grade which corresponds to the following point value: • A = 2 points, • B = 1 point, • C = no points. The detailed criteria for individual grades (and for code quality in general) are provided in ‹5_quality.txt›. If your grade is not A, your tutor will point out what you need to improve. You then get a chance (once) to improve your code and submit the task for a second round of review. If your code has sufficiently improved, you can get the next better grade (i.e. B if your first grade was C and A if your first grade was B). ### Peer Review Reading code is an important skill – sometimes more so than writing it. While the space to practice reading code in this subject is limited, you will be able to earn a few points doing just that. The rules for peer review are quite different from those for teacher reviews above: • you can submit any code (even completely broken) for peer review, • to write a review for any given submission, you must have already passed the respective assignment yourself, • there are no specific deadlines for requesting or providing peer reviews. It is okay to point out correctness problems during peer reviews, with the expectation that this might help the recipient pass the assignment. This is the «only» allowed form of cooperation (more on that below). ### Examples A lot more work is available that what you need to do, even for an A. We do not expect you to solve all the exercises nor tasks – pick a subset you like, but be sure to spread the work through the entire semester (there are very significant bonuses for doing it this way). To give you an idea, there are some point calculations: • below tables give achievable grades by passed task count, • the specific grade in the range depends on the outcome of your exam (8pt – 18pt), • 4 tasks are the minimum to pass, ◦ all 3 sets must be covered, ◦ the 2 that are alone in their set must get 1pt or better review, • 5 tasks are more or less the expected count, • solving 6 tasks (i.e. half of them) allows considerable leeway in other semester work, • 6 tasks is also the minimum to get an A. Maxing out all the optional work gives you this: │ tasks │ pts │ rev │ other │ total │ grade │ ├┄┄┄┄┄┄┄│┄┄┄┄┄│┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ 3 │ 9 │ 6 │ 24 │ 39 │ X │ │ 4 │ 12 │ 8 │ 24 │ 44 │ E-C │ │ 5 │ 15 │ 10 │ 24 │ 49 │ D-B │ │ 6 │ 18 │ 12 │ 24 │ 54 │ C-A │ │ 7 │ 21 │ 12 │ 24 │ 57 │ B-A │ │ 8 │ 24 │ 12 │ 24 │ 60 │ A │ If you forfeit 2 weeks of seminars and the second ‘activity’ point and get, on average, a B on teacher reviews, these are your prospects (you do need to distribute work across task sets, to get the 2 point bonus on all of them): │ tasks │ pts │ rev │ other │ total │ grade │ ├┄┄┄┄┄┄┄│┄┄┄┄┄│┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ 4 │ 12 │ 4 │ 21 │ 37 │ X │ │ 5 │ 15 │ 5 │ 21 │ 41 │ E-C │ │ 6 │ 18 │ 6 │ 21 │ 45 │ D-B │ │ 7 │ 21 │ 6 │ 21 │ 48 │ D-B │ │ 8 │ 24 │ 6 │ 21 │ 51 │ C-A │ │ 9 │ 27 │ 6 │ 21 │ 54 │ C-A │ ## Task Sets The general principles outlined here apply to all tasks. The first and most important rule is, use common sense – the specifications are not exhaustive and sometimes leave room for different interpretations. Do your best to apply the most sensible one. Do not try to find loopholes (all you are likely to get is failed tests). Technically correct is «not» the best kind of correct. Think about pre- and postconditions. Aim for weakest preconditions that still allow you to guarantee the postconditions required by the assignment. If your preconditions are too strong (i.e. you disallow inputs that are not ruled out by the spec) you will likely fail the tests. Do not print anything that you are not specifically directed to. Programs which print garbage (i.e. anything that wasn't specified) will fail tests. You can use the standard C++ library. External libraries or header files are not allowed, unless specified as part of the assignment. Make sure that your classes and methods use the correct spelling, and that you accept and/or return the correct types. In most cases, either the ‘syntax’ or the ‘sanity’ test suite will catch problems of this kind, but we cannot guarantee that it always will – do not rely on it. If you don't get everything right the first time around, do not despair. The expectation is that most of the time, you will pass in the second or third verity run (especially if you test your program carefully). If you strongly disagree with a test outcome and you believe you adhered to the specification and resolved any ambiguities in a sensible fashion, please raise the issue in the discussion forum. ### Submitting Solutions The easiest way to submit, for instance, a solution to the task ‹t1_cellular› is this: $ ssh aisa.fi.muni.cz $ cd ~/pb161/t1 … edit files until satisfied … $ pb161 submit t1_cellular NB. Only the files listed in the assignment will be submitted and evaluated. Please put your entire solution into existing files (or into files you are instructed to create). You can check the status of your submissions by issuing the following command: $ pb161 status In case you already submitted a solution, but later changed it, you can see the differences between your most recent submitted version and your current version by issuing: $ pb161 diff t1_cellular The lines starting with ‹-› have been removed since the submission, those with ‹+› have been added and those with neither are common to both versions. ### Compilation To compile and test your solution, use the ‹make› command: each ‹tX› directory has a ‹makefile› in it. Typing ‹make cellular› in this directory will first compile your solution into an executable binary and then run ‹clang-tidy›, any tests you may have written, and ‹valgrind›. If you want to work on your own computer instead of ‹aisa›, you need to figure out the settings yourself. The ‹makefile› will tell you which compiler we use and how we invoke it. ### Evaluation There are three sets of automated tests which are executed on the solutions you submit. The first set is called ‘syntax’ and runs immediately after you submit. Only 2 checks are performed: the code compiles and it passes ‹clang-tidy›. The next step is ‘sanity’ and runs every midnight and noon. Its main role is to check that your program meets basic semantic requirements, e.g. that it recognizes correct inputs and produces correctly formatted outputs. The ‘sanity’ test suite is for your information only and does not guarantee that your solution will be accepted. The ‘sanity’ test suite is only executed if you passed ‘syntax’. The ‘verity’ test suite covers most of the specified functionality and runs once or twice a week (the exact schedule is in the previous section). If you pass the verity suite, the assignment is considered complete and you are awarded the points. The verity suite will «not» run unless the code passes ‘sanity’ (with the exceptions specified in the task descriptions). Please note that any memory errors (including memory leaks, as reported by ‹valgrind›) will cause ‘verity’ to fail. Only the most recent submission is evaluated, and each submission is evaluated at most once in the ‘sanity‘ and once in the ‘verity’ mode. You will find your latest evaluation results in the IS in notepads (one notepad per task). ## Peer Reviews You can optionally participate in peer reviews, both as a reviewer and as a review recipient. While reviewers get points for their effort, the recipients do not – instead, they get (hopefully) useful information. ### Requesting Reviews If you would like to have your code reviewed, you can issue the following command: $ pb161 review --request t1_cellular Substitute other programming tasks for ‹t1_cellular› as appropriate. You can request a «peer» review on a task which you did not pass yet. You may get up to 3 reviews for any given request. The reviewer will work with the submission that was current at the time you have created the request. Make sure you submit the code you want reviewed before requesting the review. The ‹pb161 update› command will indicate whether someone reviewed your code, by printing a line of the form ‹A reviews/t1_cellular.by.xlogin›. To read the review, look at the files in ‹~/pb161/reviews/t1_cellular.by.xlogin› -- you will find a copy of your submitted sources along with comments provided by the reviewer. After you read your review, you should write a few sentences for the reviewer into ‹note.txt› in the review directory (please wrap lines to 80 columns) and then run: $ pb161 review --accept 100 Instead of 100, you can use a smaller number, indicating what percentage of the points the reviewer deserves for their job. Please make sure that you grade the review honestly -- the reviews will be screened for abuse and depending on the type of misconduct, one or both parties will be punished. To request a review from a «teacher» (as opposed to peer review), add ‹--teacher› to the command: $ pb161 review --request t1_cellular --teacher The output from ‹pb161 status› will indicate the task submissions for which you have requested a teacher review. ### Writing Reviews To participate as a reviewer, start with the following command: $ pb161 review --list You will get a list of review requests for which you are an eligible reviewer. In particular, only tasks that you have already successfully solved will show up. If you like one of the entries, note its number (e.g. 7) and type: $ pb161 review --checkout 7 $ cd ~/pb161/reviews/ $ ls There will be a directory for each of the reviews you agreed to write. Each directory contains the source code submitted for review, along with further instructions (the file ‹readme.txt›). When inserting your comments, please use double ‹**› to make the comment stand out, like this: /** A short, one-line remark. **/ or for longer comments: /** A longer comment, which should be wrapped to 80 columns or ** less, and where each line should start with the ** marker. ** It is okay to end the comment on the last line of text like ** this. **/ ## Code Quality As mentioned earlier, when you submit your code for teacher review, it will be graded A–C. The following criteria apply. ### Vices This is a list of things your code should not do, and the best grade that is possible if they make an appearance. In all cases, only ‘nontrivial’ instances matter, but unfortunately, there is no obvious line between trivial and nontrivial. Your reviewer's judgment will apply. • Code duplication: this is a very serious problem, both when the code is literal copy&paste and when there are minor modifications between the copies. Any significant duplication is an automatic «C». Code which is highly redundant (multiple implementations of the same concept or pattern, even if not literally copied) is still a problem, including duplication of the standard library. All but trivial cases warrant a «C». • Spaghetti: another common and very serious problem, often paired with the previous. Long functions, an excessive number of local variables, non-obvious side effects which affect control flow down the line, functions which do too many things at once. A minor instance caps your grade at «B», anything more than that means a «C». • Bad naming caps your grade at «B» (if the problem is pervasive, a «C» is very likely). This includes: ◦ meaningless names – single-letter global variables, names which say nothing about the purpose of the thing (‹tmp1› through ‹tmp7›, ‹tmp›, ‹tmptmp›, ‹pom›, ‹pomoc›, …), ◦ names which are not English nor established placeholders or abbreviations (these are fine: ‹a›, ‹b› for arguments of binary operators, ‹i›, ‹j› for loop variables, etc.), ◦ overlong, completely redundant names for local objects (‹first_plus_operand›, ‹loop_index_variable_1›). • Inappropriate data types, data structures or algorithms: using building blocks which do not fit the intended purpose makes programs hard to follow and reason about, and often also leads to poor performance. Abuse of strings is especially common. Caps the grade at «B» (but may contribute strongly to a «C»). If your code is free of the above vices, it will get a «B» or an «A», depending on the virtues described below. ### Virtues To earn a grade better than «C», your code should be free from vices and also demonstrate some of the following virtues. • Cohesion and orthogonality: each code unit (class, function, …) does one well-defined thing and has a clear and fitting name. Required for «A». • Good naming: names should be clear, descriptive, respect word categories (based around verbs for functions and nouns for types and variables), be free from spelling or grammatical errors. Names should not be redundant – context matters. The verbosity of a name should be inversely proportional to its scope. Do not repeat established context (no ‹list::list_length›). Required for «A». • Comments: each non-obvious code unit should have a comment concisely describing what it does and why. Contributes towards an «A», but is not required. Comments which establish correctness of the code are especially valued. • Preconditions: each function should clearly state its preconditions, preferably in executable form (‹assert›). Contributes significantly toward an «A». ## Exam The raison d'être of this course is to teach you to write correct C++ programs on your own – and the programming test is designed to ensure that this was indeed the outcome for you personally. Of course, we recognize that there is additional pressure when you are programming for an exam. You will get plenty of time to solve the exercises (in relation to their difficulty). During the exam, it'll be possible to submit the solutions and get back results of a ‘sanity’ test. The ‘sanity’ assertions will also be included with the exam source files, for your convenience. There is no requirement to pass ‹clang-tidy›. The exam will take place on 31st of May and 2nd of June (Tuesday and Thursday, respectively) starting at 12:30 and will extend until 17:00. Additional attempts will be possible on 14th and 28th of June. You will get a ‘yes/no’ verity result (without any indication of what went wrong) at 14:00 and 15:30, and full results at 17:00. You will also get a chance for a ‘rehearsal’: there are two practice exams in an appendix of this document (directory ‹pex›). You can work them out and submit them for evaluation, as if they were a real exam. This is strictly optional and will not be graded in any way. It is up to you to complete them within a reasonable time limit (e.g. 3 hours, 2/3 of the official time limit for the real exam) and on your own. You can submit multiple times for the practice tests and get ‘verity’ test results immediately, but please keep in mind that this will «not» be possible at the actual exam. ### Evaluation The programming test will be evaluated using automated tests, just like the 12 ‘major’ tasks. Each exercise is evaluated in a binary fashion: you «must» pass all tests in order to succeed on the given task, in which case you are awarded its points. If you fail to obtain 8 points (i.e. pass 2 out of the 4 exercises), you get an F and you can try again according to the standard rules for repeating exams. ### Materials The exam will be offline. The exam computers will have a standard selection of text editors and development tools¹ installed, in their default configurations. This document (but without exercise solutions) and lecture slides (again without example source code) will be available for reference. ¹ You can expect VS Code, Geany, ‹micro› and ‹vim› to be present. Qt Creator might be available. CLion has been excluded due to stability issues (we don't want to deal with any more crashes in the middle of an exam than we absolutely have to). ## Plagiarism tl;dr: Please work alone and do not cheat. Cheating is a colossal waste of everyone's time. We would prefer to spend that time on improving the course for everyone. Thank you. And now for the long version, because sadly, the above is not enough. The goal of this subject is to teach you to write programs in C++ – from understanding the problem, through designing the solution and writing it down in C++. You must be able to do all of this «on your own». Teamwork has its place, but it's not in this subject. You must work out all «graded» exercises and tasks entirely on your own. Discussing the solution, even in abstract terms, is not permitted. If you do not understand something, ask your tutor «privately». If you are caught cheating, ‘we have only shared ideas’ or even ‘we only discussed the problem statement’ will not hold as a valid defence. If you want to study together, that is fine, and encouraged – there are plenty of «ungraded» exercises for this purpose. You can discuss those, solve them together, share and compare your solutions and so on. Please note that you are also responsible for keeping your solutions private. If you only use the ‹pb161› command on ‹aisa›, it will make your ‹~/pb161› directory inaccessible to anyone else (this also applies to school-provided UNIX workstations). Keep it that way. If you work on your solution using other computers, make sure they are secure. Do not publish your solutions anywhere (on the internet or otherwise) and do not share them for any reason. All parties in a copying incident will be treated equally. ### Penalties Any points awarded for work that has been shared with another student are voided (including any points from reviews, and any other bonuses they enabled – that is, if you copy a prep exercise and you had been awarded a point for that week's seminar, that point will be revoked). Additionally, following penalties apply: │ items copied │ penalty │ total │ ├─────────────▻│────────▻│──────▻│ │ 1st task │ -3pt │ -3pt │ │ 2nd task │ -5pt │ -8pt │ │ 3rd task │ -7pt │ -15pt │ │┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ 1st prep ex. │ -1pt │ -1pt │ │ 2nd prep ex. │ -2pt │ -3pt │ │ 3rd prep ex. │ -3pt │ -6pt │ │ 4th prep ex. │ -5pt │ -11pt │ The ‘counter’ is shared between tasks and exercises (with tasks coming first), so if you copy (or let someone copy, same thing) a task and two exercises, that'd be -3 (1st task) + -2 (2nd exercises) + -3 (3rd exercise), for a total of -8pt. # Strings and Classes Welcome to PB161. If you haven't read the rules and guidelines in Part A (directory ‹00› in the source bundle), please do so now, before going on. The exercises this week will look at some of the basics that you have seen in the lecture: strings, dynamic arrays – ‹std::vector›, classes with methods, and ‹const› references. These concepts are further explored in the ‘demonstrations’ (commented examples; please note that these do not replace the lectures – they are meant to be complementary). The corresponding files in the source bundle are named ‹d?_*.cpp›, i.e. ‹d1_fibonacci.cpp› through ‹d4_lemmings.cpp›. 1. ‹fibonacci› – using ‹std::vector› (dynamic array) 2. ‹hamming› – introduction to ‹std::string› 3. ‹hero› – introduction to object composition 4. ‹lemmings› – collections of custom objects The second part of the study materials for each week gives you a couple of ‘elementary’ exercises, which you should be able to quickly solve to make sure you understand the concepts from the lecture and from the commented examples above. Sample solutions are in Part S at the end of the PDF, or in the directory ‹sol› in the source code bundle. Please note that the sample solution is not always the simplest possible – it's fine to take a more roundabout approach. The source files for this section are named ‹e?_*.cpp›. This week, the elementary exercises are: 1. ‹predicates› – properties of lists of numbers 2. ‹palindrome› – checking that an ‹std::string› is a palindrome 3. ‹pascal› – fill in an ‹std::vector› The next section has slightly more difficult exercises. These are labelled «preparatory», since they exist to let you prepare for the corresponding seminar. You are strongly encouraged to solve at least 3 of them every week (if you submit them by Saturday, you can then gain a point for attending the seminar). The respective source files in the bundle are called ‹p?_*.cpp›. «Note:» Discussing and sharing solutions is strictly forbidden – you must solve the exercises on your own. For details, see Part A (directory ‹00› in the source bundle). 1. ‹counting› – count words and lines in a string 2. ‹fraction› – evaluate a continued fraction 3. ‹words› – break a string into a vector of one-word strings 4. ‹account› – encapsulation of state, ‹const› methods 5. ‹shapes› – object composition 6. ‹contacts› – collections of your own objects The last section has «regular» exercises – those will be (on average) yet more difficult and let you further practice programming with the concepts that you have learned this week. Like with elementary exercises earlier, solutions to these can be freely discussed and shared, you can work on the exercises with your friends, and you can compare your solution to those included in Part S. Some of these exercises will be solved interactively in the seminar. The files are named ‹r?_*.cpp›. 1. ‹wrap› – wrap long lines into paragraphs of a given width 2. ‹digits› – representing numbers in a positional system 3. ‹sieve› – find prime numbers 4. ‹bsearch› – binary search in a sorted ‹std::vector› 5. ‹qsort› – the staple of in-place sorting algorithms 6. ‹radix› † – a fast comparison-free sorting algorithm ## a. Using the source bundle We recommend that you work on ‹aisa›, which has all the required tools installed and set up correctly. You can use ‹micro› as your editor if you are not familiar with ‹vim›, or you can use a remote editing feature in your code editor of choice. If you prefer to set up your own, local, environment, you are of course free to do that, but please keep in mind that your tools are your responsibility. If you work on ‹aisa›, you can check your solutions against the test cases provided with the exercises by running ‹make› (this tries to compile all of them in order) or ‹make p1_counting› to compile and test only one of them. Additionally, ‹make› will run your code through ‹clang-tidy› and ‹valgrind›. When you are satisfied with the solution, you can use ‹pb161 submit› in the directory of the particular unit. This week, that's ‹~/pb161/01›. You can consult ‹pb161 status› to confirm that your submission is in the system. Thus, the entire sequence looks like this: $ pb161 update $ cd ~/pb161/01 … edit code until tests pass / you are satisfied … $ pb161 submit $ pb161 status You can see test results in the notepads in the IS. As an alternative, you can use the following command to submit the solution and display the test results as soon as they become available: $ pb161 submit --wait ## d. Demonstrations ### [‹fibonacci›] We will assume some familiarity with C (or at least some C-style braces-and-semicolons language, like Java). First things first: subroutines, statements, types and «values». In C++, variables, containers and so on hold «values». Assignment updates those values and does not rebind the name to a different object. If you come from Java or Python, this is a bit of a culture shock. See also lectures. In this demo, we will implement the mother of all programming language examples, the Fibonacci sequence (forget hello world, this is not that kind of a course). First the function (subroutine) «signature» -- in order come: • «return type» – in this case ‹std::vector< int >›, then • the name of the subroutine – ‹fibonacci›, and finally • the argument list: ‹int n›. std::vector< int > fibonacci( int n ) /* C */ A vector is a «sequence container» -- it holds a sequence of values. In C++, containers are «generic», that is, parametrized by the type of their elements, and these type parameters are specified in angle brackets. In this case, we are declaring that ‹fibonacci› returns a vector (sequence) of integers (‹int› is the ‘default’ integer type in C++). The curly braces after the signature enclose the function «body». { /* C */ The body is a sequence of statements, separated by semicolons (with the exception of compound statements – which are enclosed in braces and are «not» followed by a semicolon). The first statement in this function is a «local variable declaration», which consists of the «type» (the already familiar ‹std::vector< int >›), possible «declarators» (like pointers and references… again, we will get to those later – there are none in this particular case) and the «name» of the variable: ‹fib›. std::vector< int > fib; /* C */ Vectors are generalized arrays: unlike traditional C arrays, they can be resized on demand. To set the size of a vector, we can use its method ‹resize›: to call a «method» of an object (and vector is an object), we use the following syntax: • the variable holding the object – ‹fib›, then • a dot, • then the name of the method to call, • then an argument list, enclosed in parentheses. Of course, like everything in C++, method calls can get a lot more complicated, and it is a topic that we will likewise revisit. fib.resize( n, 1 ); /* C */ Now that ‹fib› is an appropriately sized vector, with the number 1 stored at each index, we can go on to rewrite the values to the actual Fibonacci sequence. We will use a ‹for› loop, which you probably know from C – the ‹for› statement has 3 sections enclosed in parentheses and separated by semicolons: • initialisation, which usually declares or initializes the loop variable, • the loop condition, • the iteration expression, which is executed after every iteration, before the loop condition. The head of the loop is followed by a statement, which is the «body»: the code that is repeatedly executed. Often, this is a compound statement (enclosed in braces) but it doesn't have to. for ( int i = 2; i < n; ++ i ) /* C */ In this case, the body consists of a single statement: an assignment, which updates the ‹i›-th position in the vector ‹fib› with the sum of the values stored at the two preceding indices. Square brackets after a variable name indicate the «indexing operator» and works analogously to array indexing in C. fib[ i ] = fib[ i - 1 ] + fib[ i - 2 ]; /* C */ The return statement does two things, like in most imperative languages: it provides the «return value», and it immediately stops execution of the function, transferring control back to the caller. return fib; /* C */ } All demonstrations and exercises in this collection contain a short collection of «test cases». In the demos, they usually serve to show how the code explained in the main part works, and for you to change and experiment. int main() /* demo */ /* C */ { std::vector fib_7{ 1, 1, 2, 3, 5, 8, 13 }; std::vector fib_1{ 1 }; assert( fibonacci( 7 ) == fib_7 ); /* C */ assert( fibonacci( 1 ) == fib_1 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹hamming›] Besides sequences of numbers, another type of sequence frequently appears in computer programs: «strings», which are made of letters. In C++, the basic data type for working with strings is ‹std::string›, and it is rather similar to a vector, though strings provide additional methods, for operations commonly performed on strings (but not so commonly on other sequences). In this demo, we will show some basic usage of ‹std::string›. The following function, called ‹hamming› returns an integer (of type ‹int›) and accepts 2 arguments. Notice that there are some new elements in the declarations of those arguments: the ‹const› qualifier, meaning that we do not intend to modify the values ‹a› and ‹b›, and a «reference declarator», denoted ‹&›. These two often go together – in this arrangement, they declare a «constant reference». In a function argument list, this means that the data will «not be copied» when the function is called, but the function promises not to change the original. Since a string might contain a lot of data, copying all of it might be expensive: this is why we prefer to use a constant reference to pass it into a function, if the function only needs to examine, but not change, the content of the string. In other words, ‹a› and ‹b› are not «values» in their own right; instead, they are aliases (new names) for existing values, albeit such that the original values «cannot be modified» through these new names. If you try to, the compiler will complain.¹ int hamming( const std::string &a, const std::string &b ) /* C */ { First, we declare a precondition: the strings must be of equal size. In other words, calling ‹hamming› on two strings of different length is a «programming» error: the caller is responsible for ensuring that the condition holds. assert( a.size() == b.size() ); /* C */ We declare a local variable to hold the computed distance, of type ‹int› (the ‘default’ integral type in C++). int distance = 0; /* C */ Again, a standard C-style ‹for› loop. Notice that strings can be indexed, just like arrays and vectors. Also notice that the loop variable is now of type ‹size_t› – an unsigned integer type. This is because the ‹size› methods of standard containers in C++ return unsigned numbers,² and comparing signed and unsigned integers can cause problems. for ( size_t i = 0; i < a.size(); ++ i ) /* C */ if ( std::toupper( a[ i ] ) != std::toupper( b[ i ] ) ) distance ++; And a return statement. return distance; /* C */ } That is all. If you have never heard of Hamming distance before, it might be a good idea to look it up. int main() /* demo */ /* C */ { assert( hamming("Python", "python") == 0 ); assert( hamming("AbCd", "aBcD") == 0 ); assert( hamming("string", "string") == 0 ); assert( hamming("aabcd", "abbcd") == 1 ); assert( hamming("abcd", "ghef") == 4 ); assert( hamming("Abcd", "bbcd") == 1 ); assert( hamming("gHefgh", "ghefkl") == 2 ); } ¹ Of course, this being C++, there is a way around that. It is only needed very rarely, and only in ‘plumbing’ – low-level code which implements, for instance, new data types. ² Arguably, this is a design mistake in C++. There are proposals to fix it, but a change in this regard is going to take a long time, if it ever happens. In the meantime, it makes sense to use unsigned types for straightforward loop variables (i.e. those that count up). ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹hero›] In many programs, pre-made data types included in the standard library are more than sufficient. However, it is also often the case that a custom data type could be useful – most often to describe a particular concept from the domain which the program models. Let us consider a dungeon crawler, or some other role-playing game set in a fantasy world. In such games, the protagonist will be able to pick up items and make use of them, for instance wield a sword to fight the critters in the dungeon. This would be a rather typical use case for a custom data type: there might be many individual swords in the game, but they all share the same essential set of attributes, like weight, or the amount of damage they deal to the opponent. Of course, we could store these attributes as a tuple, with anonymous fields, and remember that the weight is the first element and the attack strength is the second. While fine in a small program, this approach is not very scalable.¹ With ‹struct› (and ‹class›, in a short while), we can create «user-defined data types», with named «attributes» and «methods». The ‹struct› keyword is inherited from C, where it defines an «aggregate» (or record) data type. C++ extends this concept with methods, constructors, destructors, inheritance, and so on. However, at their heart, C++ objects are really just fancy record types. We will start by exploring these. A record type describes a composite (or aggregate) value, made of a fixed number of attributes (fields), possibly of different types. In this sense, it is very much like a tuple. However, in a record type, the fields have names, and their values are accessed by using those names (instead of their positions as in a tuple). To define a record type, we use the keyword ‹struct›, followed by the name of the type, followed by the definition of the individual fields. Let's start by defining a type which will describe a sword. struct sword /* C */ { The most important attribute of a sword is, clearly, a fancy name. Recall that we can use ‹std::string› to conveniently store strings. Let us then declare the attribute ‹name› of type ‹std::string›: std::string name; /* C */ Then there are some attributes that deal with game mechanics. Let us just describe them using two integers, ‹weight› and ‹attack›. In actual games, things usually get a bit more complicated. It is possible to give «default values» to attributes – in this case, when a value of type ‹sword› is created, ‹weight› and ‹attack› will be both set zero. How this is achieved or why it is important will be discussed later. int weight = 0; /* C */ int attack = 0; }; That is all. At this point, ‹sword› is a type, like ‹int› or ‹std::string›, and we can declare variables of type ‹sword›, return values of type ‹sword› from functions, or pass values of type ‹sword› as function arguments. For example, let's write a trivial predicate on values of type ‹sword›. Notice the syntax for attribute access: it is the same that we have used for calling methods of ‘built-in’ types like ‹std::string› earlier. This is not a coincidence. bool sword_is_heavy( const sword &s ) /* C */ { return s.weight > 50; } Let us define another record type, ‹shield›, before moving on. struct shield /* C */ { std::string name; int weight = 0; int defense = 0; }; Swords and shields are usually rather passive. However, programs often also model more dynamic entities; user-defined record types would seem like a good fit to describe their static side (i.e. their attributes). For instance, a ‹hero› would have a health bar (how much damage they can take before dying), and some weapons (a sword and a shield, for instance). And obviously a name. Given a record type which models an entity, it is possible, of course, to write functions which describe the «behaviours» of this entity. For instance ‹hero_walk› or ‹hero_attack› could be functions which take the specific hero to act on as one of their arguments. You perhaps notice the imbalance though: attributes use a nice and concise syntax, ‹value.attribute›, but functions use much clumsier ‹type_method( value, … )›. But we did not have to say ‹string_size( string )› earlier. Indeed, in C++, it is possible to also bundle functions into user-defined data types, in addition to attributes. Such data types are no longer called ‘record types’ – instead, they are known as «classes». In other words, a C++ class is a «user-defined data type» with «attributes» and «methods» (associated functions). Let us define one of those – the syntax is analogous to record types:² class hero /* C */ { In classes, attributes are often «private»: only methods of the same class are allowed to directly access them. This is the default: unlike ‹struct›, when we start writing declarations into a class, they will be inaccessible to the outside code. This is okay for our current purposes. It is also common practice to prefix attributes in a class (unless they are public) with an underscore, or some other short string (‹m_›, for member, is also sometimes used), to avoid naming conflicts: it is not allowed to have an attribute and a method with the same name. std::string _name; /* C */ shield _shield; sword _sword; To mark further attributes and methods as accessible to the outside world, we use the label ‹public›, like this: public: /* C */ Methods are declared just like functions, the only immediate difference being that this is done inside a class. And the odd ‹const› keyword at the end of the signature. This ‹const› tells the compiler that the method does not change the object in any way when it is called (again, this is enforced by the compiler). bool wields_heavy_sword() const /* C */ { return sword_is_heavy( _sword ); } An example of non-‹const› method would be the following, which causes the hero to wield a sword given by the argument. The method assigns into one of the attributes, which obviously changes the object, and hence cannot be marked ‹const›. void wield( const sword &s ) /* C */ { _sword = s; } Finally, we will add a «constructor»: a special kind of method which is called automatically by the compiler whenever a value of type ‹hero› is created, e.g. by declaring a local variable. The constructor's name is the name of the class, and it has no return type, bit it can have arguments. Unlike standard functions (and standard methods), constructors have an «initialization section», which can initialize attributes, e.g. by passing arguments to «their» constructors. When the body of the constructor is entered, all the attributes will have been already constructed. The initialization section starts with a colon, and is followed by a list of expressions of the form ‹attribute( argument list )›. hero( std::string name ) /* C */ : _name( name ) {} }; That is quite enough for now. Let us look at a few examples of code using the above types. int main() /* demo */ /* C */ { sword katana = { "Katana", 10, 17 }; hero protagonist( "Hiro Protagonist" ); protagonist.wield( katana ); assert( !protagonist.wields_heavy_sword() ); } ¹ After all, we could just use void pointers, remember how big the data is and which attribute is stored at which offset. There is a good reason why nobody writes serious programs in this style. ² In fact, ‹struct› and ‹class› are essentially the same thing, and only differ in minor syntactic details. Nonetheless, we will usually write ‹struct› for plain record types (without methods) and ‹class› for actual classes. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹lemmings›] While we are talking about computer games, you might have heard about a game called Lemmings (but it's not super important if you didn't). In each level of the game, lemmings start spawning at a designated location, and immediately start to wander about, fall off cliffs, drown and generally get hurt. The player is in charge of saving them (or rather as many as possible), by giving them tasks like digging tunnels, or stopping and redirecting other lemmings. Let's try to design a «class» (reminder: a «class» is a «user-defined data type» with «attributes» and «methods») which will capture the state of a single lemming: class lemming /* C */ { Each lemming is located somewhere on the map: coordinates would be a good way to describe this. For simplicity, let's say the designated spawning spot is at coordinates ⟦(0, 0)⟧. double _x = 0, _y = 0; /* C */ Unless they hit an obstacle, lemmings simply walk in a given direction – this is another candidate for an attribute; and being rather heedless, it's probably good idea to keep track of whether they are still alive. bool _facing_right = true; /* C */ bool _alive = true; Finally, they might be assigned a task, which they will immediately start performing. An «enumerated type» is another kind of a «user-defined type» and consists of a discrete set of named labels. You have most likely encountered them in C. enum task { no_task, digger, stopper, /* … */ }; /* C */ task _task = no_task; public: /* C */ Let us define a couple methods: void start_digging() { _task = digger; } /* C */ bool busy() const { return _task != no_task; } bool alive() const { return _alive; } void step() /* C */ { _x += _facing_right ? 1 : -1; _y += 0; // TODO gravity, terrain, … } }; Earlier, we have mentioned that user-defined types are essentially the same as built-in types – their values can be stored in variables, passed to and from functions and so on. There are more ways in which this is true: for instance, we can construct collections of such values. Earlier, we have seen a sequence of integers, the type of which was ‹std::vector< int >›. We can create a vector of lemmings just as easily: as an ‹std::vector< lemming >›. Let us try: int count_busy( const std::vector< lemming > &lemmings ) /* C */ { Note that the vector is marked ‹const› (because it is passed into the function as a «constant reference»). That extends to the items of the vector: the individual lemmings are also ‹const›. We are not allowed to call non-‹const› methods, or assign into their attributes here. For instance, calling ‹lemmings[ 0 ].start_digging()› would be a compile error. int count = 0; /* C */ Now is perhaps a good time to introduce a new piece of syntax: the range ‹for› loop. Its main purpose is to iterate over all items in a given collection, which is exactly what we want to do. It consists of a declaration of the loop variable, followed by a colon, and an expression which ought to yield an iterable sequence. for ( const lemming &l : lemmings ) /* C */ if ( l.busy() ) count ++; return count; /* C */ } int main() /* demo */ /* C */ { We first create an (empty) vector, then fill it in with 7 lemmings. std::vector< lemming > lemmings; /* C */ lemmings.resize( 7 ); We can call methods on the lemmings as usual, by indexing the vector: lemmings[ 0 ].start_digging(); /* C */ assert( count_busy( lemmings ) == 1 ); We can also modify the lemmings in a range ‹for› loop – notice the absence of ‹const›; this time, we use a «mutable reference» (often called just a reference, or an lvalue reference – more on that later): for ( lemming &l : lemmings ) /* C */ { assert( l.alive() ); l.start_digging(); } assert( count_busy( lemmings ) == 7 ); /* C */ } ## e. Elementary Exercises ### [‹predicates›] Write the following predicates (pure functions which return a boolean value). The first two return true if all (‹all_odd›) or at least one (‹any_odd›) number in the list is odd: bool all_odd( const std::vector< int > & ); /* C */ bool any_odd( const std::vector< int > & ); The third returns true if there are at least ‹n› numbers divisible by ‹k›: bool count_divisible( const std::vector< int > &, int k, int n ); /* C */ ### [‹palindrome›] Write a predicate which decides whether a given string is a palindrome, i.e. reads the same in both directions. bool is_palindrome( const std::string &s ); /* C */ ### [‹pascal›] Write a function which builds the ‹n›-th row of Pascal's triangle as a vector of integers and returns it. std::vector< int > pascal( int n ); /* C */ ## p. Preparatory Exercises ### [‹counting›] In this exercise, we will work with strings in a read-only way: by counting things in them. Write two functions, ‹word_count› and ‹line_count›: the former will count words (runs of characters without spaces) and the latter will count the number of non-empty lines. Use range ‹for› to look at the content of the string. Here are the prototypes of the functions -- you can simply turn those into definitions. We pass arguments by ‹const› references: for now, consider this to be a bit of syntax, the purpose of which is to avoid making a copy of the string. It will be explained in more detail later. Also notice that in a prototype, the arguments do not need to be named (but you will have to give them names to use them). int word_count( const std::string & ); /* C */ int line_count( const std::string & ); ### [‹fraction›] Write a function which evaluates a continued fraction: given a vector of coefficients of the continued fraction, it computes a numerator and a denominator of a traditional fraction with the same value. A continued fraction is a representation of a rational number ⟦q⟧ as a sum of ⟦a₀⟧ and the reciprocal of a second number, ⟦q₀⟧, which is itself written as a continued fraction: ⟦q₀ = a₀ + 1/q₁⟧ where ⟦q₁ = a₁ + 1/q₂⟧, ⟦q₂ = a₂ + 1/q₃⟧ and so on. The sequence ⟦a₀, a₁, a₂, …⟧ are the «coefficients» of the continued fraction. For a rational number, one of the ⟦qₙ⟧ eventually becomes 0 and the sequence ends there. For more details, see e.g. wikipedia. Define a traditional fraction as a ‹struct› with two integer attributes, ‹p› and ‹q› (the numerator and the denominator, respectively). struct fraction; /* C */ fraction eval_continued( const std::vector< int > &coeff ); /* C */ ### [‹words›] Write a function that breaks up a string into individual words. We consider a word to be any string without whitespace (spaces, newlines, tabs) in it. Since we are lazy to type the long-winded type for a vector of strings, we define a «type alias». The syntax is different from C, but it should be clearly understandable. We will encounter this construct many times in the future. using string_vec = std::vector< std::string >; /* C */ The output of ‹words› should be a vector of strings, where each of the strings contains a single word from ‹in›. string_vec words( const std::string &in ); /* C */ ### [‹account›] In this exercise, you will create a simple class: it will encapsulate some state (account balance) and provide a simple, safe interface around that state. The class should have the following interface: • the constructor takes 2 integer arguments: the initial balance and the maximum overdraft • a ‹withdraw› method which returns a boolean: it performs the action and returns ‹true› iff there was sufficient balance to do the withdrawal • a ‹deposit› method which adds funds to the account • a ‹balance› method which returns the current balance (may be negative) and that can be called on ‹const› instances of ‹account› class account; /* C */ ### [‹shapes›] Another exercise about objects, this time about their composition. We will write 2 classes: ‹point› and ‹rectangle›. Points have 2 coordinates (⟦x⟧ and ⟦y⟧) and rectangles are defined by 2 points (their opposing corners). Points are constructed from two doubles: the ⟦x⟧ and ⟦y⟧ coordinates, and they have ‹x()› and ‹y()› methods which return doubles. class point; /* C */ A function to compute euclidean distance between two points. Writing it is a part of the exercise, but it will be also useful when implementing the ‹diagonal› method in ‹rectangle›. double distance( point a, point b ); /* C */ Rectangles are constructed from a pair of points (bottom left and upper right corner) and provide methods: ‹width›, ‹height› and ‹diagonal› which all return a ‹double›, and a method ‹center› which returns a ‹point›. class rectangle; /* C */ ### [‹contacts›] We will look at using collections of objects. We only know one type of collection: a dynamic array, so that's what we will use. The objects we will consider are simple entries in a contact list: they have a name and a phone number (both stored as strings). We need ‹contact› to possess a two-parameter constructor (which initializes both its fields) and two getters (methods), ‹name› and ‹phone›. class contact; /* C */ using contacts = std::vector< contact >; /* type alias */ Let's write a helper function which checks whether the string ‹small› is a prefix of the string ‹big›. bool is_prefix( const std::string &small, const std::string &big ); /* C */ And finally, a function to return all contacts whose names start with the given prefix (use ‹is_prefix› in a loop). contacts search( const contacts &list, const std::string &prefix ); /* C */ ## r. Regular Exercises ### [‹wrap›] We will look at ‹std::string› again. Our first task will be to implement a simple word wrapping (paragraph filling) algorithm. «Input»: An ‹std::string› with ASCII text (letters, spaces, newlines and punctuation) and ‹columns› (a number of columns). Each line of the input text represents a single paragraph. «Output»: A string in which there are actual paragraphs with line breaks, not too far after the given column number. That is, at most a single word crosses the ‹column›-th column. Newlines in the input are replaced by double newlines in the output. std::string fill( const std::string &in, int columns ); /* C */ ### [‹digits›] Write a function to convert a number into its positional representation in a given base. Return the result as a vector, with the most significant digit first. std::vector< int > digits( int n, int base ); /* C */ ### [‹sieve›] Implement the Sieve of Eratosthenes for quickly finding the largest prime smaller than or equal to a given bound. int sieve( int bound ); /* C */ ### [‹bsearch›] Implement binary search on a vector. In this case, we will use a non-const reference to pass the vector, because we don't know yet how to deal with const iterators properly. We also don't know how to write generic algorithms (we will see that at the end of this course), so we use a vector of integers. It is customary to return the «end» iterator if an element is not found. A pair of iterators in C++, by convention, denotes a left-closed / right-open interval, like this: [begin, end). std::vector< int >::iterator bsearch( std::vector< int > &vec, int val ); /* C */ ### [‹qsort›] Implement recursive quicksort on a vector of integers. The algorithm proceeds as follows: 1. if the array has 0 or 1 element, it is already sorted: stop, 2. otherwise select one of the elements as a pivot, 3. rearrange the vector (array) into two smaller «partitions», such that all items smaller than the pivot go into the left partition, then comes the pivot, then the right partition with the remaining (greater or equal) elements, 4. recursively quicksort the left and the right partition (excluding the pivot). Useful invariant: after each partition, the pivot is at its correct index in the final sorted order. See also: https://xkcd.com/1185/ void quicksort( std::vector< int > &vec ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹radix›] † Most sorting algorithms that you encounter are so-called ‘comparison sorts’: they perform pairwise comparisons to determine the correct order of elements. It is a well-known result that an optimal sort of this type will perform O(nlogn) comparisons. However, many types of data have additional properties which make linear-time – O(n) – sorting possible. One such algorithm is «radix sort» and it works for numbers and strings. In this exercise, we will use it to sort numbers. Like quicksort, the algorithm has 2 basic components: a helper function ‹sort_by_digit› which performs a «stable» sort only looking at a ⟦i⟧-th digit (letter) in each of the numbers (strings). This can be done in linear time because there is only a fixed number of digits (or letters): 1. in one pass, count the number of keys for each possible value of the given (⟦i⟧-th) digit, 2. in a second pass, sort the input into pre-determined buckets (filling the buckets in the iteration order, ensuring stability of the sort). The buckets are then concatenated to form the digit-sorted sequence (though they can also be allocated as a consecutive block upfront, saving a temporary copy). The main procedure then simply performs ‹sort_by_digit› on each digit, starting from the least significant. The result is a sorted array. void radixsort( std::vector< unsigned > &to_sort, int base = 10 ); /* C */ # References and Lambdas In this chapter, we will work with references (both constant and mutable) and also look at the basics of higher-order functions in C++. Demonstrations: 1. ‹stats› – input and output parameters 2. ‹primes› – fill in a vector with prime numbers 3. ‹iterate› – building sequences by iterating a function 4. ‹newton› – a general routine for numeric approximation Elementary exercises: 1. ‹fibonacci› – old sequence, new function signature 2. ‹normalize› – divide out the gcd from a fraction 3. ‹accumulate› – sum up ⟦f(x)⟧ for all ⟦x⟧ in an ‹std::vector› Preparatory exercises: 1. ‹rewrap› – word wrapping redux, this time in-place 2. ‹golden› – basic uses of output parameters 3. ‹divisors› – collections as in/out parameters 4. ‹midpoints› – in/out parameters of custom types 5. ‹higher› – higher-order function primer: ‹map› and ‹zip› 6. ‹fixpoint› – find a fixed point of a monotonic function Regular exercises: 1. ‹euler› – implement Euler's totient function ⟦φ⟧ 2. ‹approx› – somewhat easier approximation 3. ‹solve› – a very simple game solver 4. ‹sort› – selection sort with a comparator 5. ‹permute› – compute a vector of digit permutations 6. ‹bsearch› – binary search with a comparator ## d. Demonstrations ### [‹stats›] In this demo, we will do some basic descriptive statistics. Last week, we have used «constant references» to pass «input» arguments into functions. We will now see how to use non-constant (mutable) references to implement «output» and «in/out» arguments. The syntax for a mutable reference is simply the type, the reference declarator (‹&›) and the name of the argument, i.e. dropping the ‹const› (compare ‹data› vs ‹median› in the following function signature. void stats( const std::vector< double > &data, /* C */ double &median, double &mean, double &stddev ) { int n = data.size(); double sum = 0, square_error_sum = 0; for ( double x_i : data ) /* C */ sum += x_i; Notice that we do not «read» the value of ‹median› before overwriting it with the resulting value: this is a hallmark of an «output argument» – it is never read before being written by the function. mean = sum / n; /* C */ if ( n % 2 == 1 ) /* C */ median = data[ n / 2 ]; else median = ( data[ n / 2 ] + data[ n / 2 - 1 ] ) / 2; However, after we have assigned a value to ‹mean›, we can continue to use it like a normal read-write variable. It is important that the read cannot be reached without executing the write first (e.g. it would be a problem if the write above was conditional). for ( double x_i : data ) /* C */ square_error_sum += ( x_i - mean ) * ( x_i - mean ); double variance = square_error_sum / ( n - 1 ); /* C */ stddev = std::sqrt( variance ); No return statement: the function was declared with ‹void› as its return type, meaning that it does not return anything. The values are all passed to the caller via output arguments. } /* C */ int main() /* demo */ /* C */ { double median, mean, stddev; std::vector< double > sample = { 2, 4, 4, 4, 5, 5, 5, 7, 9 }; stats( sample, median, mean, stddev ); assert( mean == 5 ); /* C */ assert( median == 5 ); assert( stddev == 2 ); sample.push_back( 1100 ); /* C */ stats( sample, median, mean, stddev ); assert( median == 5 ); /* C */ assert( mean > 100 ); assert( stddev > 100 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹primes›] Besides simple output arguments, like in the previous demo, we can pass values out of functions by manipulating existing objects, most straightforwardly containers. In this demo, we will write a function ‹primes› which appends prime numbers from a given range to an existing ‹std::vector›. We will still call ‹out› an «output argument», though the concept is clearly more nuanced here. Like before, we will use a mutable reference to achieve the desired semantics. void primes( int from, int to, std::vector< int > &out ) /* C */ { for ( int candidate = from; candidate < to; ++ candidate ) { bool prime = true; int bound = std::sqrt( candidate ) + 1; Decide whether a given number is prime, naively, by trial division. for ( int div = 2; div < bound; ++ div ) /* C */ if ( div != candidate && candidate % div == 0 ) { prime = false; break; } Now the interesting part: if the number was found to be prime, we append it to the object referenced by ‹out› (i.e. the original object which was declared outside this function and passed into it by reference). Below in ‹main›, you can see that the content of the vector ‹p_out› changes when we call this function on it. if ( prime ) /* C */ out.push_back( candidate ); } } int main() /* demo */ /* C */ { std::vector< int > p_out; std::vector< int > p7 = { 2, 3, 5 }, p15 = { 2, 3, 5, 7, 11, 13 }; primes( 2, 7, p_out ); /* C */ assert( p_out == p7 ); primes( 7, 15, p_out ); assert( p_out == p15 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹iterate›] In this short demo, we will introduce new syntax for writing functions. The type of function we will use is called a «lambda», from the symbol that is used in «lambda calculus» to introduce anonymous functions. In C++, lambdas are like regular functions with a few extras. Notice that ‹iterate› is declared as a «variable» – the function is on the right-hand side, and does not have an intrinsic name (i.e. it is anonymous). The «type» of ‹iterate› is not specified – instead, we have used ‹auto›, to instruct the compiler to fill in the type. Besides the missing name and the empty square brackets, the signature of the lambda is similar to a standard function. However, on closer inspection, another thing is missing: the return type. This might be specified using ‹-> type› after the argument list, but if it is not, the compiler will, again, deduce the type for us. The return type is commonly omitted. auto iterate = []( auto f, auto x, int count ) /* C */ An advantage of a «lambda» is that we do not need to know the types of all the arguments in advance: in particular, we don't know the type of ‹f› – this will most likely be a lambda itself (i.e. ‹iterate› is a higher-order function). When this is the case, instead of the type, we specify ‹auto›, instructing the compiler to deduce a type when the function is used. This is the same principle which we have applied to the «variable» ‹iterate› itself: we do not know the type, so we ask the compiler to fill it in for us (by using ‹auto›). Let us continue by writing the body of ‹iterate›: { /* C */ We want to build a vector of values, starting with ‹x›, then ‹f(x)›, ‹f(f(x))›, and so on. Immediately, we face a problem: what should be the type of the vector? We need to specify the type parameter to declare the variable, and this time we won't be able to weasel out by just saying ‹auto›, since the compiler can't tell the type without an unambiguously typed initializer. We have two options here: 1. in some circumstances, it is possible to omit the type parameter of ‹std::vector› and let the compiler deduce only that. This would be written ‹std::vector out{ x }› – by putting ‹x› into the vector right from the start, the compiler can deduce that the element type should be the same as the type of ‹x›, whatever that is; we will deal with this mechanism much later in the course (in the last block); in the meantime, 2. we can use ‹decltype› to obtain the type of ‹x› and use that to specify the required type parameter for ‹out›, i.e.: std::vector< decltype( x ) > out; /* C */ out.push_back( x ); We build the return vector by repeatedly calling ‹f› on the previous value, until we hit ‹count› items. for ( int i = 1; i < count; ++ i ) /* C */ out.push_back( f( out.back() ) ); And we return the value, like in a regular function. Please also note the semicolon after the closing brace: definition of a lambda is an «expression», and the variable declaration as a whole needs to be delimited by a semicolon, just like in ‹int x = 7;›. return out; /* C */ }; int main() /* demo */ /* C */ { auto f = []( int x ) { return x * x; }; auto g = []( int x ) { return x + 1; }; Of course, we can use ‹auto› in declaration of regular variables too, as long as they are initialized. auto v = iterate( f, 2, 4 ); /* C */ std::vector< int > expect{ 2, 4, 16, 256 }; /* C */ assert( v == expect ); std::vector< int > /* C */ iota = iterate( g, 1, 4 ), iota_expect{ 1, 2, 3, 4 }; assert( iota == iota_expect ); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹newton›] This demonstration is as far as we'll venture with regards to numeric approximation – the exercises that deal with approximations are all much simpler than this demo. Here, we will implement the general Newton-Raphson method. This can be used for finding all kinds of roots (zeroes of functions) numerically and for solving ‘hard’ (transcendental) equations. The input to Newton's method is a function ‹f› and its derivative, ‹df›. A single improvement step then takes the current estimate ⟦x₀⟧ and subtracts ⟦f(x)/df(x)⟧ from it. It is actually quite simple. auto newton = []( auto f, auto df, double ini, double prec ) /* C */ { double x = ini, y = ini - f( x ) / df( x ); while ( std::fabs( y - x ) >= prec ) /* C */ { x = y; y = y - f( x ) / df( x ); } return y; /* C */ }; We can straightforwardly apply the above generic function to suitable arguments to immediately implement some familiar functions, like square roots or cube roots (we just need to find a function which becomes zero if ⟦x⟧ is the square root of the argument of the function; that function would be ⟦f(z) = z² - x⟧ and its derivative is ⟦f'(z) = 2z⟧). double sqrt( double x, double prec ) /* square root */ /* C */ { return newton( [=]( double z ) { return z * z - x; }, [=]( double z ) { return 2 * z; }, 1, prec ); } double cbrt( double n, double prec ) /* cube root */ /* C */ { return newton( [=]( double z ) { return z * z * z - n; }, [=]( double z ) { return 3 * z * z; }, 1, prec ); } Compute nth root of x, generalizing ‹sqrt› and ‹cbrt› above. double root( int n, double x, double prec ) /* C */ { auto f = [=]( double z ) { return std::pow( z, n ) - x; }; auto df = [=]( double z ) { return n * std::pow( z, n - 1 ); }; return newton( f, df, 1, prec ); } Scroll to the end to see the test cases. The following code computes π using only basic arithmetic and the Newton method… It's all a bit fast and loose, but it works. Enjoy. Approximately evaluate a function using its truncated Taylor expansion. auto taylor = []( auto coeff, double x, double prec ) /* C */ { double r = 0, pow = 1, fact = 1; int i = 0; while ( pow / fact > prec / 10 ) /* C */ { r += coeff( i ) * pow / fact; fact *= ++i; pow *= x; } return r; /* C */ }; Shorthand for 4-periodic Taylor coefficients (like those that appear in trigonometric functions). auto trig_coeff( int a, int b, int c, int d ) /* C */ { return [=]( int i ) { return i % 4 == 0 ? a : i % 4 == 1 ? b : i % 4 == 2 ? c : d; }; } Sine and cosine, to feed into Newton. double sine( double x, double prec ) /* C */ { return taylor( trig_coeff( 0, 1, 0, -1 ), x, prec ); } double cosine( double x, double prec ) /* C */ { return taylor( trig_coeff( 1, 0, -1, 0 ), x, prec ); } Compute π/2 as the root of cosine. double pi( double prec ) /* C */ { auto f = [=]( double x ) { return cosine( x, prec ); }; auto df = [=]( double x ) { return -sine( x, prec ); }; return 2 * newton( f, df, 1, prec ); } int main() /* demo */ /* C */ { for ( int decimals = 1; decimals < 10; ++ decimals ) { double p = std::pow( 10, -decimals ); assert( std::fabs( sqrt( 2, p ) - std::sqrt( 2 ) ) < p ); /* C */ assert( std::fabs( cbrt( 2, p ) - std::cbrt( 2 ) ) < p ); assert( std::fabs( root( 2, 2, p ) - std::sqrt( 2 ) ) < p ); /* C */ assert( std::fabs( root( 3, 2, p ) - std::cbrt( 2 ) ) < p ); assert( std::fabs( root( 4, 16, p ) - 2 ) < p ); assert( std::fabs( pi( p ) - 4 * std::atan( 1 ) ) < p ); /* C */ } } ## e. Elementary Exercises ### [‹fibonacci›] Fill in an existing vector with a Fibonacci sequence (i.e. after calling ‹fibonacci( v, n )›, the vector ‹v› should contain the first ‹n› Fibonacci numbers, and nothing else). // void fibonacci( … ) /* C */ ### [‹normalize›] Write a function to normalize a fraction, that is, find the greatest common divisor of the numerator and denominator and divide it out. Both numbers are given as in/out parameters. // void normalize( … ) /* C */ ### [‹accumulate›] Write a function ‹accumulate( f, vec )› which will sum up ⟦f(x)⟧ for all ⟦x⟧ in the given ‹std::vector< int > vec›. // auto accumulate = … /* C */ ## p. Preparatory Exercises ### [‹rewrap›] A different take on word-wrapping. The idea is very similar to last week – break lines at the first opportunity after you ran out of space in your current line. The twist: do this by modifying the input string. Additionally, undo existing line breaks if they are in the wrong spot. void rewrap( std::string &str, int cols ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹golden›] The function ‹next_fib› should behave like this: • given: ‹a == fib( i )› and ‹b == fib( i + 1 )› • execute: ‹next_fib( a, b )› • to get: ‹a == fib( i + 1 )› and ‹b == fib( i + 2 )›. void next_fib( int &a, int &b ); /* C */ «Optional»: Compute the n-th Fibonacci number using next_fib. Make it so that: ‹fib( 1 ) == 1›, ‹fib( 2 ) == 1›, ‹fib( 3 ) == 2›. This is just to practice working with ‹next_fib› in case you aren't sure. int fib( int n ); /* C */ Approximate the golden ratio as the ratio of two consecutive Fibonacci numbers. The ‹precision› argument gives an upper bound on the approximation error. The number ‹rounds› is an output parameter and gives us the number of iterations (calls to ‹next_fib›) that were required to satisfy the precision requirement. Notice that: • the golden mean ⟦φ = 1.618⟧... • ‹fib( 2 ) / fib( 1 )› = 1 / 1 = 1 is a lower bound • ‹fib( 3 ) / fib( 2 )› = 2 / 1 = 2 is an upper bound • ‹fib( 4 ) / fib( 3 )› = 3 / 2 = 1.5 is a lower bound • ‹fib( 5 ) / fib( 4 )› = 5 / 3 = 1.667 is an upper bound and so on. Surely the error – distance from ⟦φ⟧ itself – in any given round is smaller than its distance from the previous round. double golden( double precision, int &rounds ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹divisors›] Take a number, find all its «prime» divisors and add them into ‹divs›, unless they are already there. Be sure to do this in time proportional (linear) to the input number. «Bonus»: If you assume that ‹divs› is sorted in ascending order when you get it, you can make ‹add_divisors› a fair bit more efficient. Can you figure out how? void add_divisors( int num, std::vector< int > &divs ); /* C */ ### [‹midpoints›] A familiar class: add a 2-parameter constructor and x(), y() accessors (the coordinates should be double-precision floating point numbers). class point; /* C */ Consider a closed shape made of line segments. Replace each segment A with one that starts at the midpoint of A and ends at the midpoint of B, the segment that comes immediately after A. The input is given as a sequence of points (each point shared by two segments). The last segment goes from the last point to the first point (closing the shape). void midpoints( std::vector< point > &pts ); /* C */ helper functions for floating-point almost-equality bool near( double a, double b ) /* C */ { return std::fabs( a - b ) < 1e-8; } bool near( point a, point b ) /* C */ { return near( a.x(), b.x() ) && near( a.y(), b.y() ); } ### [‹higher›] Write a map function, which takes a function ‹f› and a vector ‹v› and returns a new vector ‹w› such that w[ i ] = f( v[ i ] ) for any valid index ‹i›. We will need to use the ‘lambda’ syntax for this, since we don't yet know any other way to write functions which accept functions as arguments. // static auto map = []( ... ) { ... }; /* C */ Similar, but ‹f› is a binary function, and there are two input vectors of equal length. You do not need to check this. // static auto zip = []( ... ) { ... }; /* C */ You can assume that the output vector is of the same type as the input vector (i.e. ‹f› is of type a → a in map, and of type a → b → a for ‹zip›. ### [‹fixpoint›] A fixed point of a function ⟦f⟧ is an ⟦x⟧ such that ⟦f(x) = x⟧. A function is monotonic if ⟦∀x, y. x ≤ y → f(x) ≤ f(y)⟧. Assume that ‹f› is a monotonic function and hence, since there are only finitely many ‹int› values, that it has at least one fixed point. Find the «greatest» fixed point of ‹f›. // auto fixpoint = … /* C */ int f( int x ) { return x / 2; } /* C */ int g( int x ) { return x - x / 20; } int h( int x ) { return std::max( x / 5, 20 ); } int i( int x ) { return x < INT_MAX ? x + 1 : x; } ## r. Regular Exercises ### [‹euler›] This is a straightforward math exercise. Implement Euler's [φ], for instance using the product formula ⟦φ(n) = nΠ(1 - 1/p)⟧ where the product is over all distinct prime divisors of n. You may need to take care to compute the result exactly. long phi( long n ); /* ref: 21 lines */ /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹approx›] Remember ‹fib.cpp›? We can do a bit better. Let's decompose our golden() function differently this time. The ‹approx› function is a higher-order one. What it does is it calls f() repeatedly to improve the current estimate, until the estimates are sufficiently close to each other (closer than the given precision). The ‹init› argument is our initial estimate of the result. // auto approx = []( auto f, double init, double prec ) { ... }; /* C */ Use ‹approx› to compute the golden mean. Note that you don't need to use the previous estimate in your improvement function. Use by-reference captures to keep state between iterations if you need some. double golden( double prec ); /* C */ The Babylonian (Heron) method to compute square roots. Please take note, you may find it helpful later. This is how ‹approx› is supposed to be used. double sqrt( double n, double prec ) /* C */ { auto improve = [=]( double last ) { double next = n / last; return ( last + next ) / 2; }; return approx( improve, 1, prec ); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹solve›] Consider a single-player game that takes place on a 1D playing field like this: ╭───────────────╮ │ ▼ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐ │ 2 │ 4 │ … │ … │ │ 2 │ │ … │ 0 │ └───┴───┴───┴───┴───┴───┴───┴───┴───┘ │ ▲ ▲ │ │ ▲ ╰───────╯ ╰──────╯ ╰──────╯ The player starts at the leftmost cell and in each round can decide whether to jump left or right. The playing field is given by the input vector ‹jumps›. The size of the field is ‹jumps.size() + 1› (the rightmost cell is always 0). The objective is to visit each cell exactly once. bool solve( std::vector< int > jumps ); /* C */ ### [‹sort›] Implement an in-place selection sort of a vector of integers, using a «comparator» passed to the sort routine as an argument. A comparator is a function that is used to compare elements instead of the builtin relational operators. This is useful if your data is sorted in non-standard manner. // auto selectsort = []( std::vector< int > &to_sort, auto cmp ) …; /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹permute›] Given a number ‹n› and a base ‹b›, find all numbers whose digits (in base ‹b›) are a permutation of the digits of ‹n› and return them as a vector of integers. Each such number should appear exactly once. Examples: (125)₁₀ → { 125, 152, 215, 251, 512, 521 } (1f1)₁₆ → { (1f1)₁₆, (f11)₁₆, (11f)₁₆ } (20)₁₀ → { 20, 2 }  std::vector< unsigned > permute_digits( unsigned n, int base ); /* C */ std::vector< unsigned > to_digits( unsigned n, int base, int fill = 0 ) /* C */ { std::vector< unsigned > ds; while ( n > 0 || fill > 0 ) /* C */ { ds.push_back( n % base ); n /= base; -- fill; } return ds; /* C */ } void check( unsigned n, int base, /* C */ const std::vector< unsigned > &expect ) { auto got = permute_digits( n, base ); std::set< unsigned > uniq( got.begin(), got.end() ); assert( got.size() == expect.size() ); /* C */ assert( got.size() == uniq.size() ); auto n_digits = to_digits( n, base ); /* C */ std::sort( n_digits.begin(), n_digits.end() ); for ( unsigned p : got ) /* C */ { auto p_digits = to_digits( p, base, n_digits.size() ); std::sort( p_digits.begin(), p_digits.end() ); assert( n_digits == p_digits ); } } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹bsearch›] Implement binary search on a vector, with a twist: the order of the elements is given by a «comparator». This is a function that is passed as an argument to ‹search› and is used to compare elements instead of the builtin relational operators. This is useful if your data is sorted in non-standard manner. // auto search = []( std::vector< int > &vec, int val, auto cmp ); /* C */ # Containers This week will be about containers (collections). Demonstrations: 1. ‹freq› – a word frequency histogram 2. ‹dfs› – reachability using recursive depth-first search 3. ‹closure› – closure properties of relations 4. ‹bfs› – find the nearest matching node Elementary exercises: 1. ‹unique› – remove duplicated entries from a vector 2. ‹reflexive› – compute a reflexive closure of a relation 3. ‹normalize› – scale an input vector into a 0-1 range Preparatory exercises: 1. ‹brackets› – check that brackets in a string balance out 2. ‹connected› – decompose a graph into connected components 3. ‹dag› – check whether a graph is acyclic (dfs again) 4. ‹rel› – a tiny bit of relational algebra 5. ‹numbers› – a slightly enriched set of numbers 6. ‹bipartite› – bipartiteness checking using BFS Regular exercises: 1. ‹mode› – find the mode (most common value) in a vector 2. ‹buckets› – sort items into buckets based on an attribute 3. ‹shortest› – shortest distances in an unweighted graph 4. ‹flood› – flood fill in a grid 5. ‹colour› – brute-force a 3-colouring of a graph 6. ‹life› – the game of life ## d. Demonstrations ### [‹freq›] In this demo, we will build up a histogram of word appearances. Since we will want to process the input incrementally, we will implement the word counter as a class with 2 methods: ‹process›, which will add each word that appears in its argument to the histogram, and ‹count›, which takes a single-word string as an argument and returns how many times it has been encountered by ‹process›. class freq /* C */ { All the heavy lifting will be done by the standard associative container, ‹std::map›. We will use ‹std::string› as the key type (holding the word of interest) and ‹int› as the value type: the number of appearances of this word. std::map< std::string, int > _counter; /* C */ public: /* C */ We will first implement a helper method, for counting a single word. It will be convenient to ignore empty strings here, so we will do just that. Notice that we use the indexing (subscript) operator to access the value which ‹std::map› associates with the given key. Also notice that the key is automatically added to the map in case it is not yet present. void add( const std::string &word ) /* C */ { if ( !word.empty() ) _counter[ word ] ++; } Now the main workhorse: ‹process› takes an input string, decomposes it into individual words and counts them. Notice the use of ‹+=› to append a letter to an existing string. void process( const std::string &str ) /* C */ { std::string word; for ( auto c : str ) /* C */ if ( std::isblank( c ) ) { add( word ); word.clear(); } else word += c; Do not forget to add the last word, in case it was not followed by a blank. add( word ); /* C */ } We would clearly like to mark the ‹count› method, which simply returns information about the observed frequency of a word, as ‹const›. However, the subscript operator on an ‹std::map› is not ‹const› – this is because, as we have mentioned earlier, should the key not be present in the map, it will be added automatically, thus changing the content of the container. Instead, we can ask ‹std::map› to check whether the key is present (by using ‹count›), without adding it. If the key is missing, we simply return 0. Otherwise, we ask the map to find the value associated with the key, again without adding it if it is missing. Note that dereferencing the result of ‹find› is undefined if the key is not present (in this case, we know for sure that the key is present – we just checked). All ‹std::map› methods which we used are marked ‹const› and hence we can mark our ‹count› method ‹const› as well, as we desired. int count( const std::string &s ) const /* C */ { return _counter.count( s ) ? _counter.find( s )->second : 0; } }; /* C */ int main() /* demo */ /* C */ { freq f; We create a ‹const› alias for ‹f›, so that we check that it is indeed possible to call ‹count› on it. const freq &cf = f; /* C */ assert( cf.count( "hello" ) == 0 ); /* C */ assert( cf.count( "" ) == 0 ); f.process( "hello world" ); /* C */ assert( cf.count( "hello" ) == 1 ); assert( cf.count( "hell" ) == 0 ); assert( cf.count( "world" ) == 1 ); assert( cf.count( " world" ) == 0 ); f.process( "hello hello" ); /* C */ assert( cf.count( "hello" ) == 3 ); assert( cf.count( "world" ) == 1 ); f.process( "world hello world hello world" ); /* C */ assert( cf.count( "hello" ) == 5 ); assert( cf.count( "world" ) == 4 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹dfs›] In this demo, we will do some basic exploration of directed graphs. Probably the simplest possible algorithm for that is «recursive depth-first search», so that is what we will use. We will be interested in the question ‘is vertex ⟦a⟧ reachable from vertex ⟦b⟧?’. The input graph is given using adjacency lists: the ‹graph› type gives the successors for each vertex present in the graph. Please note that in principle, the set of vertices does not need to be contiguous, or composed of only small numbers (hence the ‹std::map› and not an ‹std::vector›). using edges = std::vector< int >; /* C */ using graph = std::map< int, edges >; Besides the graph itself, we will need to represent the «visited set» – the set of vertices that have already been visited by the algorithm. In a graph with loops, not keeping track of this information would lead to infinite recursion. In an acyclic graph, it could still lead to exponential running time. Since in pseudocode, this is a literal set, using ‹std::set› sounds like a good idea. Indeed, ‹std::set› is a container which keeps at most one copy of any element, and provides efficient (logarithmic time) lookup and insertion. using visited = std::set< int >; /* C */ The main recursive function needs 2 auxiliary arguments: the set of already-visited vertices ‹seen› and the boolean ‹moved›, which guards against the case when we ask whether a vertex is reachable from itself – this is traditionally only answered in affirmative when there is a path from that vertex to itself, but a naive solution would always answer ‹true›. Hence, we need to ensure at least one edge was traversed before returning ‹true›. The question this function answers is ‘is there a path which starts in vertex ‹from›, does not visit any of the vertices in ‹seen› and ends in ‹to›?’ Notice that ‹seen› is passed around by reference: there is only a single instance of this set, shared by all recursive calls. That is, if one branch of the search visits a vertex, it will also be avoided by any subsequent sibling branches (not just by the recursive calls made within the branch). bool is_reachable_rec( const graph &g, int from, int to, /* C */ visited &seen, bool moved ) { The base case of the recursion is when we reach the target vertex and have already traversed at least one edge. In this case, we return ‹true›: we have found a path connecting the two vertices. if ( from == to && moved ) /* C */ return true; The main loop looks at each successor of ‹from› and calls ‹is_reachable› recursively, asking whether there is a path from the successor to the goal state, avoiding the current state. The result of the ‹g.at› call is a (reference to) the ‹edges› container (i.e. the ‹std::vector› of vertices). Hence ‹next› ranges over the successors of the vertex ‹from›. for ( auto next : g.at( from ) ) /* C */ In case ‹next› was not yet seen (it is not present in the visited set), mark it as visited and proceed to explore it recursively. if ( !seen.count( next ) ) /* C */ { seen.insert( next ); if ( is_reachable_rec( g, next, to, seen, true ) ) return true; } We have failed to find a satisfactory path, having exhausted all the options. Return ‹false›. return false; /* C */ } Finally, we provide a simple wrapper around the recursive function above, providing initial values for the two auxiliary arguments. Check whether ‹to› can be reached by following one or more edges if we start at ‹from›. bool is_reachable( const graph &g, int from, int to ) /* C */ { visited seen; return is_reachable_rec( g, from, to, seen, false ); } int main() /* demo */ /* C */ { graph g{ { 1, { 2, 3, 4 } }, { 2, { 1, 2 } }, { 3, { 3, 4 } }, { 4, {} }, { 5, { 3 }} }; assert( is_reachable( g, 1, 1 ) ); /* C */ assert( !is_reachable( g, 4, 4 ) ); assert( is_reachable( g, 1, 2 ) ); assert( is_reachable( g, 1, 3 ) ); assert( is_reachable( g, 1, 4 ) ); assert( !is_reachable( g, 4, 1 ) ); assert( is_reachable( g, 3, 3 ) ); assert( !is_reachable( g, 3, 1 ) ); assert( is_reachable( g, 5, 4 ) ); assert( !is_reachable( g, 5, 1 ) ); assert( !is_reachable( g, 5, 2 ) ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹closure›] In this demo, we will check closure properties of relations: reflexivity, transitivity and symmetry. A relation is a set of pairs, and hence we will represent them as ‹std::set› of ‹std::pair› instances. We will work with relations on integers. Recall that ‹std::set› has an efficient membership test: we will be using that a lot in this program. using relation = std::set< std::pair< int, int > >; /* C */ The first predicate checks reflexivity: for any ⟦x⟧ which appears in the relation, the pair ⟦(x, x)⟧ must be present. Besides membership testing, we will use structured bindings and range ‹for› loops. Notice that a braced list of values is implicitly converted to the correct type (‹std::pair< int, int >›). bool is_reflexive( const relation &r ) /* C */ { Structured bindings are written using ‹auto›, followed by square brackets with a list of names to bind to individual components of the right-hand side. In this case, the right-hand side is the «loop variable» – i.e. each of the elements of ‹r› in turn. for ( auto [ x, y ] : r ) /* C */ { if ( !r.count( { x, x } ) ) return false; if ( !r.count( { y, y } ) ) return false; } We have checked all the elements of ‹r› and did not find any which would violate the required property. Return ‹true›. return true; /* C */ } Another, even simpler, check is for symmetry. A relation is symmetric if for any pair ⟦(x, y)⟧ it also contains the opposite, ⟦(y, x)⟧. bool is_symmetric( const relation &r ) /* C */ { for ( auto [ x, y ] : r ) if ( !r.count( { y, x } ) ) return false; return true; } Finally, a slightly more involved example: transitivity. A relation is transitive if ⟦∀x, y, z. (x, y) ∈ r ∧ (y, z) ∈ r → (x, z) ∈ r⟧. bool is_transitive( const relation &r ) /* C */ { for ( auto [ x, y ] : r ) for ( auto [ y_prime, z ] : r ) if ( y == y_prime && !r.count( { x, z } ) ) return false; return true; } int main() /* demo */ /* C */ { relation r_1{ { 1, 1 }, { 1, 2 } }; assert( !is_reflexive( r_1 ) ); assert( !is_symmetric( r_1 ) ); assert( is_transitive( r_1 ) ); relation r_2{ { 1, 1 }, { 1, 2 }, { 2, 2 } }; /* C */ assert( is_reflexive( r_2 ) ); assert( !is_symmetric( r_2 ) ); assert( is_transitive( r_2 ) ); relation r_3{ { 2, 1 }, { 1, 2 }, { 2, 2 } }; /* C */ assert( !is_reflexive( r_3 ) ); assert( is_symmetric( r_3 ) ); assert( !is_transitive( r_3 ) ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹bfs›] The goal of this demonstration will be to find the shortest distance in an unweighted, directed graph: 1. starting from a fixed (given) vertex, 2. to the nearest vertex with an odd value. The canonical ‘shortest path’ algorithm in this setting is «breadth-first search». The algorithm makes use of two data structures: a «queue» and a «set», which we will represent using the standard C++ containers named, appropriately, ‹std::queue›¹ and ‹std::set›. In the previous demonstration, we have represented the graph explicitly using adjacency list encoded as instances of ‹std::vector›. Here, we will take a slightly different approach: we well use ‹std::multimap› – as the name suggests, it is related to ‹std::map› with one crucial difference: it can associate multiple values to each key. Which is exactly what we need to represent an directed graph – the values associated with each key will be the successors of the vertex given by the key. using graph = std::multimap< int, int >; /* C */ The algorithm consists of a single function, ‹distance_to_odd›, which takes the graph ‹g›, as a constant reference, and the vertex ‹initial›, as arguments. It then returns the sought distance, or if no matching vertex is found, -1. int distance_to_odd( const graph &g, int initial ) /* C */ { We start by declaring the «visited set», which prevents the algorithm from getting stuck in an infinite loop, should it encounter a cycle in the input graph (and also helps to keep the time complexity under control). std::set< int > visited; /* C */ The next piece of the algorithm is the «exploration queue»: we will put two pieces of information into the queue: first, the vertex to be explored, second, its BFS distance from ‹initial›. std::queue< std::pair< int, int > > queue; /* C */ To kickstart the exploration, we place the initial vertex, along with distance 0, into the queue: queue.emplace( initial, 0 ); /* C */ Follows the standard BFS loop: while ( !queue.empty() ) /* C */ { auto [ vertex, distance ] = queue.front(); queue.pop(); To iterate all the successors of a vertex in an ‹std::multimap›, we will use its ‹equal_range› method, which will return a pair of «iterators» – generalized pointers, which support a kind of ‘pointer arithmetic’. The important part is that an iterator can be incremented using the ‹++› operator to get the next element in a sequence, and dereferenced using the unary ‹*› operator to get the pointed-to element. The result of ‹equal_range› is a pair of iterators: • begin, which points at the first matching key-value pair in the multimap, • end, which points «one past» the last matching element; clearly, if ‹begin == end›, the sequence is empty. Incrementing ‹begin› will eventually cause it to become equal to ‹end›, at which point we have reached the end of the sequence. Let's try: auto [ begin, end ] = g.equal_range( vertex ); /* C */ for ( ; begin != end; ++ begin ) /* C */ { In the body loop, ‹begin› points, in turn, at each matching key-value pair in the graph. To get the corresponding value (which is what we care about), we extract the second element: auto [ _, next ] = *begin; /* C */ if ( visited.count( next ) ) /* C */ continue; /* skip already-visited vertices */ First, let us check whether we have found the vertex we were looking for: if ( next % 2 == 1 ) /* C */ return distance + 1; Otherwise we mark the vertex as visited and put it into the queue, continuing the search. visited.insert( next ); /* C */ queue.emplace( next, distance + 1 ); } } We have exhausted the queue, and hence all the vertices reachable from ‹initial›, without finding an odd-valued one. Indicate failure to the caller. return -1; /* C */ } int main() /* demo */ /* C */ { graph g{ { 1, 2 }, { 1, 6 }, { 2, 4 }, { 2, 5 }, { 6, 4 } }, h{ { 8, 2 }, { 8, 6 }, { 2, 4 }, { 2, 5 }, { 5, 8 } }, i{ { 2, 4 }, { 4, 2 } }; assert( distance_to_odd( g, 1 ) == 2 ); /* C */ assert( distance_to_odd( g, 2 ) == 1 ); assert( distance_to_odd( g, 6 ) == -1 ); assert( distance_to_odd( h, 8 ) == 2 ); /* C */ assert( distance_to_odd( h, 5 ) == 3 ); assert( distance_to_odd( i, 2 ) == -1 ); } ¹ Strictly speaking, ‹std::queue› is not a container, but rather a container «adaptor». Internally, unless specified otherwise, an ‹std::queue› uses another container, ‹std::deque› to store the data and implement the operations. It would also be possible, though less convenient, to use ‹std::deque› directly. ## e. Elementary Exercises ### [‹unique›] Filter out duplicate entries from a vector, maintaining the relative order of entries. Return the result as a new vector. std::vector< int > unique( const std::vector< int > & ); /* C */ ### [‹reflexive›] Build a reflexive closure of a relation given as a set of pairs, returning the result. using relation = std::set< std::pair< int, int > >; /* C */ relation reflexive( const relation &r ); /* C */ ### [‹normalize›] Given a vector of non-negative floating-point numbers, produce a new vector where all entries fall into the 0-1 range, and they are all related to the original entries by the same factor. using signal_t = std::vector< double >; /* C */ signal_t normalize( const signal_t & ); /* C */ ## p. Preparatory Exercises ### [‹brackets›] Check that curly and square brackets in a given string balance out correctly. bool balanced( const std::string & ); /* C */ ### [‹connected›] Decompose an undirected graph into connected components (described by a set of sets of numbers). The graph is given as a symmetric adjacency matrix. Vertices are numbered from 1 to ⟦n⟧ where ⟦n⟧ is the dimension of the input matrix. using graph = std::vector< std::vector< bool > >; /* C */ using component = std::set< int >; /* C */ using components = std::set< component >; components decompose( const graph &g ); /* C */ ### [‹dag›] Another exercise for graph exploration, this time we will look for cycles. There are a few algorithms to choose from, those based on DFS are probably the most straightforward. This time, the input graph is given as a «multimap»: a map which can contain multiple values for each key. In other words, it behaves as a set of pairs with additional support for efficient retrieval based on the value of the first field of the pair. The ‹is_dag› function should return ‹false› iff ‹g› contains a cycle. The graph does not need to be connected. using graph = std::multimap< int, int >; /* C */ bool is_dag( const graph &g ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹rel›] This exercise demonstrates use of ‹std::tuple› and structured bindings. Since we cannot write generic code yet (and even if we did, writing the below operators in full generality would be rather tricky), we will only work with a fixed set of types (relations). First a bunch of type aliases: ‹item› and its variants each represent a single row, while ‹rel› and its variants represent an entire relation. using item = std::tuple< std::string, int, double >; /* C */ using item_dbl = std::tuple< std::string, double >; using item_int = std::tuple< std::string, int >; using rel = std::set< item >; /* C */ using rel_dbl = std::set< item_dbl >; using rel_int = std::set< item_int >; Projections: keep a subset of columns, in this case the string and either of the numeric columns. rel_int project_int( const rel & ); /* C */ rel_dbl project_dbl( const rel & ); Selection: keep a subset of rows -- those that match on the given column. Throw away all the rest. rel select_str( const rel &, const std::string &n ); /* C */ rel select_int( const rel &, int n ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹numbers›] The class represents a set of integers; operations: • ‹add› -- adds a number, returns true if it was new • ‹del› -- removes a number, returns true if it was present • ‹del_range› -- removes numbers within given bounds (inclusive) • ‹merge› -- adds all numbers from another instance • ‹has› -- returns true if the given number is in the set Complexity requirements: • ‹del_range› and ‹merge› must run in O(n) time • everything else in O(logn) time class numbers; /* C */ ### [‹bipartite›] using edges = std::vector< int >; /* C */ using graph = std::map< int, edges >; Check whether a given graph is bipartite. The graph is undirected, i.e. its adjacency relation is symmetric. bool is_bipartite( const graph &g ); /* C */ ## r. Regular Exercises ### [‹mode›] Find the mode (most common value) in a non-empty vector and return it. If there are more than one, return the smallest. int mode( const std::vector< int > & ); /* C */ ### [‹buckets›] Sort stones into buckets, where each bucket covers a range of weights; the range of stone weights to put in each bucket is given in an argument – a vector with one element per bucket, each element a pair of min/max values (inclusive). Assume the bucket ranges do not overlap. Stones are given as a vector of weights. Throw away stones which do not fall into any bucket. Return the weights of individual buckets. using bucket = std::pair< int, int >; /* C */ std::vector< int > sort( const std::vector< int > &stones, /* C */ const std::vector< bucket > &buckets ); ### [‹shortest›] Compute single-source shortest path distances for all vertices in an unweighted directed graph. The graph is given using adjacency (successor) lists. The result is a map from a vertex to its shortest distance from ‹initial›. Vertices which are not reachable from ‹initial› should not appear in the result. using edges = std::vector< int >; /* C */ using graph = std::map< int, edges >; std::map< int, int > shortest( const graph &g, int initial ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹flood›] In this exercise, we will implement a simple flood fill: in its most common formulation, this is an algorithm which: 1. is given a bitmap (a rectangular grid of pixels), 2. a starting position in the grid, 3. a fill colour, and changes the entire contiguous single-colour area that contains the starting position to use the fill colour. We will do a monochromatic version of the same (pixels are either white or black, or rather 0 or 1) and instead of modifying the input grid, or building a new one, we will simply count how many cells change colour. Example (the starting position was 1, 3): 0 1 2 3 4 5 6 0 1 2 3 4 5 6 ┌───┬───┬───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┬───┐ 0 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ │ 1 │ 1 │ 1 │ 1 │ 0 │ 0 │ 0 │ ├───┼───┼───┼───┼───┼───┼───┤ ├───┼───┼───┼───┼───┼───┼───┤ 1 │ 0 │ 1 │ 0 │ 1 │ 0 │ 0 │ 0 │ │ 1 │ 1 │ 1 │ 1 │ 0 │ 0 │ 0 │ ├───┼───┼───┼───┼───┼───┼───┤ ├───┼───┼───┼───┼───┼───┼───┤ 2 │ 0 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ ├───┼───┼───┼───┼───┼───┼───┤ ├───┼───┼───┼───┼───┼───┼───┤ 3 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ ├───┼───┼───┼───┼───┼───┼───┤ ├───┼───┼───┼───┼───┼───┼───┤ 4 │ 1 │ 1 │ 1 │ 0 │ 1 │ 1 │ 1 │ │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ ├───┼───┼───┼───┼───┼───┼───┤ ├───┼───┼───┼───┼───┼───┼───┤ 5 │ 0 │ 0 │ 0 │ 0 │ 1 │ 1 │ 1 │ │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ └───┴───┴───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┴───┘ Notice that the flood also proceeds along diagonals (i.e. from position (2, 3) to (3, 4) and then further to (4, 3)). The grid is given as a single, flat vector and a width. You can assume that the size of the vector is evenly divisible by the given ‹width›. The ‹x0› and ‹y0› give the starting position, while the last argument, ‹fill›, the colour (0 or 1) to use. using grid = std::vector< bool >; /* C */ int flood( const grid &pixels, int width, /* C */ int x0, int y0, bool fill ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹colour›] Write a function to decide whether a given graph can be 3-colored. A correct colouring is an assignment of colours to vertices such that no edge connects vertices with the same colour. The graph is given as a set of edges. Edges are represented as pairs; assume that if ⟦(u, v)⟧ is a part of the graph, so is ⟦(v, u)⟧. using graph = std::set< std::pair< int, int > >; /* C */ bool has_3colouring( const graph &g ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹life›] The game of life is a 2D cellular automaton: cells form a 2D grid, where each cell is either alive or dead. In each generation (step of the simulation), the new value of a given cell is computed from its value and the values of its 8 neighbours in the previous generation. The rules are as follows: │ state │ alive neigh. │ result │ ├───────┼──────────────┼────────┤ │ alive │ 0–1 │ dead │ │ alive │ 2–3 │ alive │ │ alive │ 4–8 │ dead │ │┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄│ │ dead │ 0–2 │ dead │ │ dead │ 3 │ alive │ │ dead │ 4-8 │ dead │ An example of a short periodic game: ┌───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───┐ │ │ │ │ │ │ ○ │ │ │ │ │ │ ├───┼───┼───┤ ├───┼───┼───┤ ├───┼───┼───┤ │ ○ │ ○ │ ○ │ → │ │ ○ │ │ → │ ○ │ ○ │ ○ │ ├───┼───┼───┤ ├───┼───┼───┤ ├───┼───┼───┤ │ │ │ │ │ │ ○ │ │ │ │ │ │ └───┴───┴───┘ └───┴───┴───┘ └───┴───┴───┘ Write a function which, given a set of live cells, computes the set of live cells after ‹n› generations. Live cells are given using their coordinates in the grid, i.e. as ‹(x, y)› pairs. using cell = std::pair< int, int >; /* C */ using grid = std::set< cell >; grid life( const grid &, int ); /* C */ # Overloading, Constructors and Lifetime First, we will look at function and method overloading, including overloading of constructors and overloading on reference kinds. We will also touch the topic of object lifetime (which considers the question of when exactly is an object valid and can be used) and ownership (controlling the lifetime of ‘subordinate’ objects, e.g. elements in a container). Demonstrations: 1. ‹art› – overloading basics, with books and paintings 2. ‹numbers› – a list of numbers which remember their type 3. ‹refs› – overloading with references 4. ‹pool› – ownership and indirect references Elementary exercises: 1. ‹diameter› – basic function overloading (circle diameter) 2. ‹circle› – same story, but with constructors 3. ‹index› – access elements of different types using indices Preparatory exercises: 1. ‹format› – method overloading 101 2. ‹least› – return a least element without making copies 3. ‹area› – geometry with function and ctor overloads 4. ‹zipper› – ‹const› method overloading on a zipper 5. ‹rpn› – postfix arithmetic with more overloading 6. ‹eval› – infix evaluation with a node pool Regular exercises: 1. ‹complex› – complex numbers and function overloading 2. ‹bsearch› – binary search versus ‹const› 3. ‹search› – binary search tree with a pool of nodes 4. ‹bitptr› – pointer to a single bit 5. ‹readint› – read integers from various string types 6. ‹sort› – choosing a sorting algorithm ## d. Demonstrations ### [‹art›] In this demo, we will look at overloading of standard toplevel functions. We will use 3 record types to represent artistic works: books of fiction, musical compositions and paintings. They have some common attributes, but they are also quite different. We will use function overloading to provide uniform access to the common attributes. We will use a very simplified view of periodization of art, one that can be more-or-less applied to all 3 types of work which we are interested in. It's perhaps important to note, that the historical periods associated with those styles do not exactly coincide in the 3 disciplines. enum class style_t /* C */ { antique, medieval, renaissance, baroque, classical, romantic, modern }; The three record types: a book has an author, a name and a publisher, along with a style. A composition additionally has a key (e.g. ‘c minor’) and a list of parts. On the other hand, a painting does not have a publisher, but we can associate a technique with it (say, ‘oil on canvas’). For simplicity, we store everything as free-form strings . struct book /* C */ { std::string author, name, publisher; style_t style; }; struct composition /* C */ { std::string author, name, key, publisher; std::vector< std::string > parts; style_t style; }; struct painting /* C */ { std::string author, name, technique; style_t style; }; Now the functions: the first will be the simplest, essentially just forwarding to attribute access. In practice, a function like this is not especially useful, but it is simple. std::string author( book b ) { return b.author; } /* C */ std::string author( composition c ) { return c.author; } std::string author( painting p ) { return p.author; } A slightly more interesting function will be ‹description›, which takes some of the attributes and combines them into a single human-readable string describing the work. std::string description( book b ) /* C */ { return b.name + " by " + b.author; } std::string description( composition c ) /* C */ { return c.name + " in " + c.key + " by " + c.author; } std::string description( painting p ) /* C */ { return p.name + " by " + p.author + " (" + p.technique + ")"; } Another attribute that is shared by books and composition is the name of the publisher. But there is no equivalent concept for paintings. What now? There are a few options: we could leave the overload undefined, which is clearly correct, but not super helpful. Or we can implement an overload which returns some placeholder value. Let's do that here. std::string publisher( book b ) { return b.publisher; } /* C */ std::string publisher( composition c ) { return c.publisher; } std::string publisher( painting ) { return "n/a"; } And finally, for the thorny issue of periods. We sort-of managed to come up with a list of periods which we can sort-of apply to everything, but the years covered differ in each discipline. So the overloads will take care of this. std::pair< int, int > period( book b ) /* C */ { switch ( b.style ) { case style_t::antique: return { -1200, 455 }; case style_t::medieval: return { 455, 1485 }; case style_t::renaissance: return { 1485, 1660 }; case style_t::baroque: return { 1600, 1680 }; case style_t::classical: return { 1660, 1790 }; case style_t::romantic: return { 1770, 1850 }; case style_t::modern: return { 1850, 2021 }; default: assert( false ); } } std::pair< int, int > period( composition c ) /* C */ { switch ( c.style ) { case style_t::antique: return { -1300, 500 }; case style_t::medieval: return { 500, 1400 }; case style_t::renaissance: return { 1400, 1600 }; case style_t::baroque: return { 1580, 1750 }; case style_t::classical: return { 1750, 1820 }; case style_t::romantic: return { 1800, 1910 }; case style_t::modern: return { 1890, 2021 }; default: assert( false ); } } std::pair< int, int > period( painting p ) /* C */ { switch ( p.style ) { case style_t::antique: return { -3000, 500 }; case style_t::medieval: return { 500, 1400 }; case style_t::renaissance: return { 1300, 1600 }; case style_t::baroque: return { 1600, 1730 }; case style_t::classical: return { 1780, 1850 }; case style_t::romantic: return { 1800, 1860 }; case style_t::modern: return { 1860, 2021 }; default: assert( false ); } } Finally, we will check that we can indeed call the functions uniformly on different types input types. int main() /* demo */ /* C */ { book antigone{ "Sophocles", "Antigone", "n/a", style_t::antique }, miserables{ "Victor Hugo", "Les Misérables", "A. Lacroix, Verboeckhoven & Cie.", style_t::romantic }; composition /* C */ bach_mass{ "J. S. Bach", "Mass", "b minor", "Bach Gesellshaft", { "soprano 1", "soprano 2", "alto", "tenor", "bass", "flute 1", "flute 2", "oboe/d'amore 1", "oboe/d'amore 2", "oboe 3", "bassoon 1", "bassoon 2", "horn", "trumpet 1", "trumpet 2", "trumpet 3", "timpani", "violin 1", "violin 2", "viola", "basso continuo" }, style_t::baroque }, fantasia{ "Bohuslav Martinů", "Fantasia H.301", "n/a", /* C */ "Max Eschig", { "theremin", "oboe", "violin 1", "violin 2", "viola", "violoncello", "piano" }, style_t::modern }; painting babel{ "Pieter Bruegel the Elder", /* C */ "The Tower of Babel", "oil on wood", style_t::renaissance }, boon{ "James Brooks", "Boon", "oil on canvas", style_t::modern }; Getting a description: assert( description( bach_mass ) == /* C */ "Mass in b minor by J. S. Bach" ); assert( description( babel ) == "The Tower of Babel by Pieter Bruegel the Elder " "(oil on wood)" ); assert( description( antigone ) == "Antigone by Sophocles" ); And periods: assert( period( bach_mass ) == std::pair( 1580, 1750 ) ); /* C */ assert( period( fantasia ) == std::pair( 1890, 2021 ) ); assert( period( boon ) == std::pair( 1860, 2021 ) ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹numbers›] In this demonstration, we will look at overloading: both of regular «methods» and of «constructors». The first class which we will implement is ‹number›, which can represent either a real (floating-point) number or an integer. Besides the attributes ‹integer› and ‹real› which store the respective numbers, the class remembers which type of number it stores, using a boolean attribute called ‹is_real›. struct number /* C */ { bool is_real; int integer; double real; We provide two constructors for ‹number›: one for each type of number that we wish to store. The overload is selected based on the type of argument that is provided. explicit number( int i ) : is_real( false ), integer( i ) {} /* C */ explicit number( double r ) : is_real( true ), real( r ) {} }; The second class will be a container of numbers which directly allows the user to insert both floating-point and integer numbers, without converting them to a common type. To make insertion convenient, we provide overloads of the ‹add› method. Access to the numbers is index-based and is provided by the ‹at› method, which is overloaded for entirely different reasons. class numbers /* C */ { The sole attribute of the ‹numbers› class is the backing store, which is an ‹std::vector› of ‹number› instances. std::vector< number > _data; /* C */ public: The two ‹add› overloads both construct an appropriate instance of ‹number› and push it to the backing vector. Nothing surprising there. void add( double d ) { _data.emplace_back( d ); } /* C */ void add( int i ) { _data.emplace_back( i ); } The overloads for ‹at› are much more subtle: notice that the argument types are all identical – there are only 2 differences, first is the return type, which however does «not participate» in overload resolution. If two functions only differ in return type, this is an error, since there is no way to select which overload should be used. The other difference is the ‹const› qualifier, which indeed does participate in overload resolution. This is because methods have a hidden argument, ‹this›, and the trailing ‹const› concerns this argument. The ‹const› method is selected when the call is performed on a ‹const› object (most often because the call is done on a constant reference). const number &at( int i ) const { return _data.at( i ); } /* C */ number &at( int i ) { return _data.at( i ); } }; int main() /* demo */ /* C */ { numbers n; n.add( 7 ); n.add( 3.14 ); assert( !n.at( 0 ).is_real ); /* C */ assert( n.at( 1 ).is_real ); assert( n.at( 0 ).integer == 7 ); /* C */ Notice that it is possible to assign through the ‹at› method, if the object itself is mutable. In this case, overload resolution selects the second overload, which returns a mutable reference to the ‹number› instance stored in the container. n.at( 0 ) = number( 3 ); /* C */ assert( n.at( 0 ).integer == 3 ); However, it is still possible to use ‹at› on a constant object – in this case, the resolution picks the first overload, which returns a constant reference to the relevant ‹number› instance. Hence, we cannot change the number this way (as we expect, since the entire object is constant, and hence also each of its components). const numbers &n_const = n; /* C */ assert( n_const.at( 0 ).integer == 3 ); // n_const.at( 1 ) = number( 1 ); this will not compile /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹refs›] In this demonstration, we will look at overloading functions based on different kinds of references. This will allow us to adapt our functions to the kind of value (and its lifetime) that is passed to them, and to deal with arguments efficiently (without making unnecessary copies). But first, let's define a few type aliases: using int_pair = std::pair< int, int >; /* C */ using int_vector = std::vector< int >; using int_matrix = std::vector< int_vector >; Our goal will be simple enough: write a function which gives access to the first element of any of the above types. In the case of ‹int_matrix›, the element is an entire row, which has some important implications that we will discuss shortly. Our main requirements will be that: 1. ‹first› should work correctly when we call it on a constant, 2. when called on a mutable value, ‹first( x ) = y› should work and alter the value ‹x› (i.e. update the first element of ‹x›). These requirements are seemingly contradictory: if we return a value (or a constant reference), we can satisfy point 1, but we fail point 2. If we return a mutable reference, point 2 will work, but point 1 will fail. Hence we need the result type to be different depending on the argument. This can be achieved by overloading on the argument type. However, we still have one problem: how do we tell apart, using a type, whether the passed value was constant or not? Think about this: if you write a function which accepts a mutable reference, it cannot be called on an argument which is constant: the compiler will complain about the value losing its ‹const› qualifier (if you never encountered this behaviour, try it out; it's important that you understand this). But that means that ‹first( int_pair &arg )› can only be called on mutable arguments, which is exactly what we need. Fortunately for us, if the compiler decides that this particular ‹first› cannot be used (because of missing ‹const›), it will keep looking for some other ‹first› that might work. You hopefully remember that ‹first( const int_pair &arg )› can be called on any value of type ‹int_pair› (without creating a copy). If we provide both, the compiler will use the non-‹const› version if it can, but fall back to the ‹const› one otherwise. And since overloaded functions can differ in their return type, we have our solution: int &first( int_pair &p ) { return p.first; } /* C */ int first( const int_pair &p ) { return p.first; } The case of ‹int_vector› is completely analogous: int &first( int_vector &v ) { return v[ 0 ]; } /* C */ int first( const int_vector &v ) { return v[ 0 ]; } Since in the above cases, the return value was of type ‹int›, we did not bother with returning ‹const› references. But when we look at ‹int_matrix›, the situation has changed: the value which we return is an ‹std::vector›, which could be very expensive to copy. So we will want to avoid that. The first case (mutable argument), stays the same – we already returned a reference in this case. int_vector &first( int_matrix &v ) { return v[ 0 ]; } /* C */ At first glance, the second case would seem straightforward enough – just return a ‹const int_vector &› and be done with it. But there is a catch: what if the argument is a temporary value, which will be destroyed at the end of the current statement? It's not a very good idea to return a reference to a doomed object, since an unwitting caller could get into serious trouble if they store the returned reference – that reference will be invalid on the next line, even though there is no obvious reason for that at the callsite. You perhaps also remember, that the above function, with a mutable reference, cannot be used with a temporary as its argument: like with a constant, the compiler will complain that it cannot bind a temporary to an argument of type ‹int_matrix &›. So is there some kind of a reference that can bind a temporary, but not a constant? Yes, that would be an «rvalue reference», written ‹int_matrix &&›. If the above candidate fails, the next one the compiler will look at is one with an rvalue reference as its argument. In this case, we know the value is doomed, so we better return a value, not a reference into the doomed matrix. Moreover, since the input matrix is doomed anyway, we can steal the value we are after using ‹std::move› and hence still manage to avoid a copy. int_vector first( int_matrix &&v ) { return std::move( v[ 0 ] ); } /* C */ If both of the above fail, the value must be a constant – in this case, we can safely return a reference into the constant. The argument is not immediately doomed, so it is up to the caller to ensure that if they store the reference, it does not outlive its parent object. const int_vector &first( const int_matrix &v ) /* C */ { return v[ 0 ]; } That concludes our quest for a polymorphic accessor. Let's have a look at how it works when we try to use it: int main() /* demo */ /* C */ { int_vector v{ 3, 5, 7, 1, 4 }; assert( first( v ) == 3 ); first( v ) = 5; assert( first( v ) == 5 ); const int_vector &const_v = v; /* C */ assert( first( const_v ) == 5 ); int_matrix m{ int_vector{ 1, 2, 3 }, v }; /* C */ const int_matrix &const_m = m; assert( first( first( m ) ) == 1 ); /* C */ first( first( m ) )= 2; assert( first( first( const_m ) ) == 2 ); /* C */ assert( first( first( int_matrix{ v, v } ) ) == 5 ); What follows is the case where the rvalue-reference overload of ‹first› (the one which handles temporaries) saves us: try to comment the overload out and see what happens on the next 2 lines for yourself. const int_vector &x = first( int_matrix{ v, v } ); /* C */ assert( first( x ) == 5 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹pool›] This demo will be our first serious foray into dealing with object lifetime. In particular, we will want to implement binary trees – clearly, the lifetime of tree nodes must exactly match the lifetime of the tree itself: • if the nodes were released too early, the program would perform invalid memory access when traversing the tree, • if the nodes are not released with the tree, that would be a memory leak – we keep the nodes around, but cannot access them. This is an ubiquitous problem, and if you think about it, we have encountered it a lot, but did not have to think about it yet: the characters in an ‹std::string› or the items in an ‹std::vector› have the same property: their lifetime must match the lifetime of the instance which «owns them». This is one of the most important differences between C and C++: if we do C++ right, most of the time, we do not need to manage object lifetimes manually. This is achieved through two main mechanisms: 1. pervasive use of «automatic variables», through «value semantics» – local variables and arguments are «automatically destroyed» when they go out of scope, 2. «cascading» – whenever an object is destroyed, its attributes are also destroyed «automatically», and a mechanism is provided for classes which own additional, non-attribute objects (e.g. elements in an ‹std::vector›) to automatically destroy them too (this is achieved by «user-defined destructors» which we will explore in part 6, two weeks from now). In general, destroying objects at an appropriate time is the job of the «owner» of the object – whether the owner is a function (this is the case with by-value arguments and local variables) or another object (attributes, elements of a container and so on). Additionally, this happens «transparently» for the user: the compiler takes care of inserting the right calls at the right places to ensure everything is destroyed at the right time. The end result is modular or «composable» resource management – well-behaved objects can be composed into well-behaved composites without any additional glue or boilerplate. To make things easy for now, we will take advantage of existing containers to do resource management for us, which will save us from writing destructors (the proverbial glue, which is boring to write and easy to get wrong). In part 7, we will see how we can use «smart pointers» for the same purpose. We will be keeping the nodes of our binary tree in an ‹std::vector› – this means that each node has an «index» which we can use to refer to that node. In other words, in this demo (and in some of this week's exercises) indices will play the role of pointers. Since 0 is a valid index, we will use -1 to indicate an invalid (‘null’) reference. Besides ‘pointers’ to the left and right child, the node will contain a single integer value. struct node /* C */ { int left = -1, right = -1; int value; }; As mentioned earlier, the nodes will be held in a vector: let's give a name to the particular vector type, for convenience: using node_pool = std::vector< node >; /* C */ Working with ‹node› is, however, rather inconvenient: we cannot ‘dereference’ the ‹left›/‹right› ‘pointers’ without going through ‹node_pool›. This makes for verbose code which is unpleasant to both read and to write. But we can do better: let's add a simple wrapper class, which will remember both a reference to the ‹node_pool› and an index of the ‹node› we are interested in: this class can then behave like a proper reference to ‹node›: only a value of the ‹node_ref› type is needed to access the node and to walk the tree. class node_ref /* C */ { node_pool &_pool; int _idx; To make the subsequent code easier to write (and read), we will define a few helper methods: first, a ‹get› method which returns an actual reference to the ‹node› instance that this ‹node_ref› represents. node &get() { return _pool[ _idx ]; } /* C */ And a method to construct a new ‹node_ref› using the same pool as this one, but with a new index. node_ref make( int idx ) { return { _pool, idx }; } /* C */ Normally, we do not want to expose the ‹_pool› or ‹node› to users directly, hence we keep them private. But it's convenient for ‹tree› itself to be able to access them. So we make ‹tree› a friend. friend class tree; /* C */ public: /* C */ node_ref( node_pool &p, int i ) : _pool( p ), _idx( i ) {} For simplicity, we allow invalid references to be constructed: those will have an index -1, and will naturally arise when we encounter a node with a missing child – that missing node is represented as index -1. The ‹valid› method allows the user to check whether the reference is valid. The remaining methods (‹left›, ‹right› and ‹value›) must not be called on an invalid ‹node_ref›. This is the moral equivalent of a null pointer. bool valid() const { return _idx >= 0; } /* C */ What follows is a simple interface for traversing and inspecting the tree. Notice that ‹left› and ‹right› again return ‹node_ref› instances. This makes tree traversal simple and convenient. node_ref left() { return make( get().left ); } /* C */ node_ref right() { return make( get().right ); } int &value() { return get().value; } /* C */ }; Finally the class to represent the tree as a whole. It will own the nodes (by keeping a ‹node_pool› of them as an attribute, will remember a «root node» (which may be invalid, if the tree is empty) and provide an interface for adding nodes to the tree. Notice that «removal» of nodes is conspicuously missing: that's because the pool model is not well suited for removals (smart pointers will be better in that regard). class tree /* C */ { node_pool _pool; int _root_idx = -1; A helper method to append a new ‹node› to the pool and return its index. int make( int value ) /* C */ { _pool.emplace_back(); _pool.back().value = value; return _pool.size() - 1; } public: /* C */ node_ref root() { return { _pool, _root_idx }; } bool empty() const { return _root_idx == -1; } We will use a vector to specify a location in the tree for adding a node, with values -1 (left) and 1 (right). An empty vector represents at the root node. using path_t = std::vector< int >; /* C */ Find the location for adding a node recursively and create the node when the location is found. Assumes that the path is correct. void add( node_ref parent, path_t path, int value, /* C */ unsigned path_idx = 0 ) { assert( path_idx < path.size() ); int dir = path[ path_idx ]; if ( path_idx < path.size() - 1 ) /* C */ { auto next = dir < 0 ? parent.left() : parent.right(); return add( next, path, value, path_idx + 1 ); } if ( dir < 0 ) /* C */ parent.get().left = make( value ); else parent.get().right = make( value ); } Main entry point for adding nodes. void add( path_t path, int value ) /* C */ { if ( root().valid() ) add( root(), path, value ); else { assert( path.empty() ); _root_idx = make( value ); } } }; int main() /* demo */ /* C */ { tree t; t.add( {}, 1 ); assert( t.root().value() == 1 ); /* C */ assert( t.root().valid() ); assert( !t.root().left().valid() ); t.add( { -1 }, 7 ); /* C */ assert( t.root().value() == 1 ); assert( t.root().left().valid() ); assert( t.root().left().value() == 7 ); t.add( { -1, 1 }, 3 ); /* C */ assert( t.root().left().right().value() == 3 ); } ## e. Elementary Exercises ### [‹diameter›] Standard point in a plane, with x and y coordinates, stored as double-precision floating point numbers, with the obvious constructor. struct point; /* C */ Define a structure which describes a circle with a given center and a given radius (a point and a non-negative number). Include a straightforward constructor. struct circle_radius; /* C */ And a structure, which describes a circle using two points: the center and a point on the circle. Again, add a constructor. struct circle_point; /* C */ Finally, define function ‹diameter› which given either of the above representations of a circle, returns its diameter (i.e. twice the radius). // double diameter( ??? ); /* C */ ### [‹circle›] Standard 2D point. struct point; /* C */ Implement a structure ‹circle› with 2 constructors, one of which accepts a point and a number (center and radius) and another which accepts 2 points (center and a point on the circle itself). Store the circle using its center and radius, in attributes ‹center› and ‹radius› respectively. struct circle; /* C */ ### [‹index›] In this exercise, you will provide index-based access to pairs and vectors of integers, using function overloading. The ‹element› function should take an ‹std::vector› or an ‹std::pair› as its first argument and an index as its second argument. A companion ‹size› function should return the number of valid indices for either of the two types of objects. // ??? element( ???, int idx ); /* C */ // ??? size( ??? ); ## p. Preparatory Exercises ### [‹format›] In this exercise, we will implement a very simple ‘string builder‘: a class that will help us create strings from smaller pieces. It will have a single overloaded method called ‹add›, in 3 variants: it will accept either a string, an integer or a floating-point number (use ‹std::to_string› for conversions). To make it easier to use, ‹add› should return a reference to the instance it was called on. See below for examples. The method ‹get› should return the constructed string. class string_builder; /* C */ ### [‹least›] The class ‹element› represents a value which, for whatever reason, cannot be duplicated. Our goal will be to write a function which takes a vector of these, finds the smallest and returns it. Do not change the definition of ‹element› in any way. class element /* C */ { int value; public: element( int v ) : value( v ) {} element( element &&v ) : value( v.value ) {} element &operator=( element &&v ) = default; bool less_than( const element &o ) const { return value < o.value; } bool equal( const element &o ) const { return value == o.value; } }; using data = std::vector< element >; /* C */ Write function ‹least› (or a couple of function overloads) so that calling ‹least( d )› where ‹d› is of type ‹data› returns the least element in the input vector. // ??? least( ??? ) /* C */ ### [‹area›] Implement 2 classes which represent 2D shapes: (regular) ‹polygon› and ‹circle›. Each of the shapes has 2 constructors: • ‹circle› takes either 2 points (center and a point on the circle) or a point and a number (radius), • ‹polygon› takes an integer (the number of sides ≥ 3) and either two points (center and a vertex) or a single point and a number (the major radius). Add a toplevel function ‹area› which can compute the area of either. struct point; /* C */ struct polygon; struct circle; ### [‹zipper›] In this exercise, we will implement a simple data structure called a «zipper» -- a sequence of items with a single «focused» item. Since we can't write class templates yet, we will just make a zipper of integers. Our zipper will have these operations: • (constructor) constructs a singleton zipper from an integer • ‹shift_left› and ‹shift_right› move the point of focus, in O(1), to the nearest left (right) element; they return true if this was possible, otherwise they return false and do nothing • ‹insert_left› and ‹insert_right› add a new element just left (just right) of the current focus, again in O(1) • ‹focus› access the current item (read and write) • bonus: add ‹erase_left› and ‹erase_right› to remove elements around the focus (return ‹true› if this was possible), in O(1) class zipper; /* C */ ### [‹rpn›] Write a simple stack-based evaluator for numeric expressions in an RPN form. The operations: • ‹push› takes a number and pushes it onto the working stack, • ‹apply› accepts an instance of one of the three operator classes defined below; like with the string builder earlier, both those methods should return a reference to the evaluator, • again like with the zipper, a ‹top› method should give access to the current top of the stack, including the possibility of changing the value, • ‹pop› which also returns the popped value, and • ‹empty› which returns a ‹bool›. All three operators are binary (take 2 arguments). struct add {}; /* addition */ /* C */ struct mul {}; /* multiplication */ struct dist {}; /* absolute value of difference */ class eval; /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹eval›] We will do an infix version of the evaluator from the previous exercise. Additionally, we will want to store common sub-expressions only once. For this reason, we will store the nodes in a pool and only take out references to them. struct node /* C */ { The type of the node. Only ‹mul› and ‹add› nodes have children. enum op_t { mul, add, constant } op; /* C */ The attributes ‹left› and ‹right› are indices, with -1 indicating an invalid (null) reference. The ‹is_root› boolean indicates whether this node is a root – that is, it does not appear as a child of any other node. int left = -1, right = -1; /* C */ bool is_root = true; The value stored in a ‹constant›-type node. int value = 0; /* C */ }; using node_pool = std::vector< node >; /* C */ An ‘ephemeral’ reference to a node – one that can be used to traverse an expression tree, but which is only valid as long as the ‹eval› instance which created it is alive. Add ‹const› methods ‹left()›, ‹right()› which return another ‹node_ref› instance, a ‹const› method ‹compute()› which evaluates the subtree, and a non-‹const› method ‹update( int )› which only works on nodes of type ‹constant›. class node_ref; /* C */ The ‹eval› class represents an entire expression which can be evaluated, traversed (starting from root nodes – those which have no parent) and, most importantly, extended by creating new nodes. class eval /* C */ { node_pool _pool; public: std::vector< node_ref > roots(); node_ref add( node_ref, node_ref ); /* C */ node_ref mul( node_ref, node_ref ); node_ref number( int ); }; ## r. Regular Exercises ### [‹complex›] Structure ‹angle› simply wraps a single double-precision number, so that we can use constructor overloads to allow use of both polar and cartesian forms to create instances of a single type (‹complex›). struct angle; /* C */ struct complex; Now implement the following two functions, so that they work both for real and complex numbers. // double magnitude( … ) /* C */ // … reciprocal( … ) The following two functions only make sense for complex numbers, where ‹arg› is the argument, normalized into the range ⟦(-π, π⟩⟧: double real( complex ); /* C */ double imag( complex ); double arg( complex ); ### [‹bsearch›] Implement binary search on a vector. Both constant and mutable vectors should be accepted (by reference) and an appropriate iterator type (‹iterator› or ‹const_iterator›) should be returned. Try to avoid code duplication. Return ‹end› if the element is not found. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹search›] Implement a binary search tree, i.e. a binary tree which maintains the search property. That is, a value of each node is: • ≥ than all values in its left subtree, • ≤ than all values in its right subtree. Store the nodes in a pool (a vector or a list, your choice). The interface is as follows: • ‹node_ref root() const› returns the root node, • ‹bool empty() const› checks whether the tree is empty, • ‹void insert( int v )› inserts a new value into the tree (without rebalancing). The ‹node_ref› class then ought to provide: • ‹node_ref left() const› and ‹node_ref right() const›, • ‹bool valid() const›, • ‹value() const› which returns the value stored in the node. Calling ‹root› on an empty tree is undefined. struct node; /* ref: 6 lines */ /* C */ using node_pool = std::vector< node >; /* C */ class node_ref; /* ref: 12 lines */ /* C */ class tree; /* ref: 28 lines */ std::tuple< bool, int, int > verify( node_ref n, int bound ); /* C */ bool has( node_ref n, int v ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹bitptr›] Implement 2 classes, ‹bitptr› and ‹const_bitptr›, which provide access to a single (mutable or constant) bit. Instances of these classes should behave as pointers in principle, though we don't yet have tools to make them behave this way syntactically (that comes next week). In the meantime, let's use the following interface: • ‹bool get()› – read the pointed-to bit, • ‹void set( bool )› – write the same, • ‹void advance()› – move to the next bit, • ‹void advance( int )› – move by a given number of bits, • ‹bool valid()› – is the pointer valid? A default-constructed ‹bitptr› is not valid. Moving an invalid ‹bitptr› results in another invalid ‹bitptr›. Otherwise, a ‹bitptr› is constructed from a ‹std::byte› pointer and an ‹int› with value between 0 and 7 (with 0 being the least-significant bit). A ‹bitptr› constructed this way is always considered valid, regardless of the value of the ‹std::byte› pointer passed to it. class bitptr; /* C */ class const_bitptr; ### [‹sort›] Implement ‹sort› which works both on vectors (‹std::vector›) and linked lists (‹std::list›) of integers. The former should use in-place quicksort, while the latter should use merge sort (it's okay to use the ‹splice› and ‹merge› methods on lists, but not ‹sort›). Feel free to refer back to ‹01/r5› for the quicksort. # T.1. Introductory Tasks The programming tasks for this block are as follows: 1. ‹cellular.*› – a simple cellular automaton simulator, 2. ‹magic.*› – a backtracking magic square solver, 3. ‹reversi.*› – a 3D version of the game reversi, 4. ‹chess.*› – a simple simulator of standard chess. In this set, the tasks only require basic programming skills and C++ constructs that you have encountered in the first two chapters. In other words, no advanced language constructs or library features are necessary. ## [‹cellular›] The goal of this task is to implement a simulator for one-dimensional cellular automata. You will implement this simulator as a class, the interface of which is described below You are free to add additional methods and data members to the class, and additional classes and functions to the file, as you see fit. You must, however, keep the entire interface in this single file. The implementation can be in either ‹cellular.hpp› or in ‹cellular.cpp›. Only these two files will be submitted. The class ‹automaton_state› represents the state of a 1D, infinite binary cellular automaton. The ‹set› and ‹get› methods can be passed an arbitrary index. class automaton_state /* C */ { Attributes are up to you. public: /* C */ automaton_state(); /* create a blank state (all cells are 0) */ void set( int index, bool value ); /* change the given cell */ bool get( int index ) const; }; The ‹automaton› class represents the automaton itself. The automaton is arranged as a cross, with a horizontal and a vertical automaton, which are almost entirely independent (each has its own state and its own rule), with one twist: the center cell (index 0 in both automata) is shared. The new state of the shared center cell (after a computation step is performed in both automata independently) is obtained by combining the two values (that either automaton would assign to that cell) using a specified boolean binary operator. The new center is obtained as ‹horizontal_center OP vertical_center›. The state can look, for example, like this: ┌───┐ │ … │ ├───┤ │ 1 │ ├───┤ │ 0 │ ┌───┬───┬───┼───┼───┬───┬───┐ │ … │ 0 │ 0 │ 1 │ 1 │ 0 │ … │ └───┴───┴───┼───┼───┴───┴───┘ │ 0 │ ├───┤ │ 1 │ ├───┤ │ … │ └───┘ The automaton keeps its state internally and allows the user to perform simulation on this internal state. Initially, the state of the automaton is 0 (false) everywhere. The rules for both the vertical and the horizontal component are given to the constructor by their Wolfram code. The center-combining operator is given by the same type of code, but instead of 3 cells, only 2 need to be combined: there are only 16 such operators (compared to 256 rules for each of the automata). The input vectors to the binary operator are numbered by their binary code as: │ left │ right │ index │ ├┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ 0 │ 0 │ 0 │ │ 0 │ 1 │ 1 │ │ 1 │ 0 │ 2 │ │ 1 │ 1 │ 3 │ The operator code is then a 4-digit binary number, e.g 0110 gives the code for ‹xor› (0 0 → 0, 0 1 → 1, 1 0 → 1, 1 1 → 0) while 1000 gives code for ‹and› (everything is zero except if both inputs are 1). And so on and so forth. The same process but with 3 input cells is used to construct the Wolfram code for the automata. class automaton /* C */ { Attributes are up to you. public: /* C */ enum direction { vertical, horizontal }; /* C */ Constructs an automaton based on a rule given by its Wolfram code for the horizontal component, another for the vertical component, and a 4-bit code for the center operator. Assume that neither of the rules contains the transition 000 → 1. automaton( int h_rule, int v_rule, int center ); /* C */ The ‹read› method returns the current value of the (shared) center cell. The set method sets the specified cell to the value given. bool read() const; /* C */ void set( direction dir, int index, bool value ); Finally, the following methods run the simulation – either perform a single step (update each cell exactly once) or a given number of steps (assume a non-negative number of steps). void step(); /* C */ void run( int steps ); }; The ‹compute_cell› function takes two rule numbers, two initial states, a center operator and a number of steps. It then computes the value of the central cell after n steps of the automaton such described. Like above, the number of steps is a non-negative number. Assume that the center cell in both input states has the same value. bool compute_cell( int vertical_rule, int horizontal_rule, /* C */ int center_op, const automaton_state &vertical_state, const automaton_state &horizontal_state, int steps ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ## [‹chess›] The goal of this task is to implement the standard rules of chess. struct position /* C */ { int file; /* column 'letter', a = 1, b = 2, ... */ int rank; /* row number, starting at 1 */ }; enum class piece_type /* C */ { pawn, rook, knight, bishop, queen, king }; enum class player { white, black }; /* C */ The following are the possible outcomes of ‹play›. The outcomes are shown in the order of precedence, i.e. the first applicable is returned. ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄▻┼◅┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤ │ ‹capture› │ the move was legal and resulted in a capture │ │ ‹ok› │ the move was legal and was performed │ │ ‹no_piece› │ there is no piece on the ‹from› square │ │ ‹bad_piece› │ the piece on ‹from› is not ours │ │ ‹bad_move› │ this move is not available for this piece │ │ ‹blocked› │ another piece is in the way │ │ ‹lapsed› │ «en passant» capture is no longer allowed │ │ ‹has_moved› │ one of the castling pieces has already moved │ │ ‹in_check› │ the player is currently in check and the │ │ │ move does not get them out of it │ │ ‹would_check› │ the move would place the player in check │ │ ‹bad_promote› │ promotion to a pawn or king was attempted │ Attempting an «en passant» when the pieces are in the wrong place is a ‹bad_move›. In addition to ‹has_moved›, (otherwise legal) castling may give: • ‹blocked› – some pieces are in the way, • ‹in_check› – the king is currently in check, • ‹would_check› – would pass through or end up in check. enum class result /* C */ { capture, ok, no_piece, bad_piece, bad_move, blocked, lapsed, in_check, would_check, has_moved, bad_promote }; struct occupant /* C */ { bool is_empty; player owner; piece_type piece; }; class chess /* C */ { public: Construct a game of chess in its default starting position. The first call to ‹play› after construction moves a piece of the white player. chess(); /* C */ Move a piece currently at ‹from› to square ‹to›: • in case the move places a pawn at its 8th rank (rank 8 for white, rank 1 for black), it is promoted to the piece given in ‹promote› (otherwise, the last argument is ignored), • castling is described as a king move of more than one square, • if the result is an error (not ‹capture› nor ‹ok›), calling ‹play› again will attempt another move by the same player. result play( position from, position to, /* C */ piece_type promote = piece_type::pawn ); Which piece is at the given position? occupant at( position ) const; /* C */ }; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ## [‹magic›] A magic square is an ⟦n × n⟧ grid of natural numbers ⟦1⟧–⟦n²⟧, such that all rows and columns and both diagonals add up to a fixed ‘magic constant’ and each number appears exactly once. Solving the square means filling in all empty cells in a manner that gives the full square the magic property. The goal of this task is to implement a simple backtracking solver for completing partially filled magic squares. class magic /* C */ { public: Construct an empty ⟦n × n⟧ square. magic( int n ); /* C */ Get the value at the given position. A return value of 0 indicates an empty square. int get( int x, int y ) const; /* C */ Set a cell at the given position to a given value. The behaviour is undefined if ‹v› is already present in the square. If ‹v› is negative, the cell is empty, but must not take ‹std::abs( v )› as its value in the solved square. void set( int x, int y, int v ); /* C */ Solve the square: fill in all empty cells so that the square has the magic property and return ‹true›. If the square cannot be solved, do not change its content and return ‹false›. bool solve(); /* C */ }; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ## [‹reversi›] The subject of this task is the game of reversi (also known as othello), played by two players on a 3D board (a box) of a given shape (given as 3 even, non-negative numbers). Size 0 in a given direction means the board is infinite in that direction. The cells are cubes (a cube has 8 vertices, 12 edges and 6 faces). The coordinates start at the center (which is a vertex) and extend in two directions (positive and negative) along the 3 axes. The 8 cells which share the center vertex have coordinates [1, 1, 1], [1, 1, -1], [1, -1, 1], [1, -1, -1], … The rules are a straightforward extension of the standard 2D rules into three dimensions: • each player starts with 4 stones placed around the center such that no two (of the same colour) share a face, with white taking the [1, 1, 1] cell, • players take turns in placing a new stone, which must be placed adjacent (share an edge, vertex or a face) to an opposing player's stone, and enemy stones must form a straight, uninterrupted line to one of current players' own stones (along straight lines – sharing a face, along diagonals which share an edge, or along diagonals which share a vertex), • the colour of all opposing stones on all such lines connecting the new stone to existing stones of the current player is flipped. The white player starts. The game ends when no new stones can be placed and the player with more stones wins. It must be possible to make a copy of an in-progress game. class reversi /* C */ { public: reversi( int x_size, int y_size, int z_size ); Place a stone at the given coordinates. If the placement was legal, returns ‹true› and the next call places a stone of the opposing player; otherwise, no change is made, the function returns ‹false› and the same player must try a different move. As a special case, if the current player has no legal move left, but the game is not finished, ‹play› must be called with ⟦x = y = z = 0⟧ to continue. Doing this is illegal in any other circumstances. It is undefined behaviour to call ‹play› when the game is already over. bool play( int x, int y, int z ); /* C */ Return true if the game is finished (no further moves are possible). bool finished() const; /* C */ Only defined if the game is already over (i.e. ‹finished› would return ‹true›). Returns the difference in the number of stones of each player: positive for white's victory, negative for black's victory, 0 for a draw. int result() const; /* C */ }; # Operators and IO The main topics for week 5 are operator overloading (which will build on what we learned about function and method overloading in week 4). The second topic for this week will be IO: we will look at formatted input and output and at reading and writing files. Demonstrations: 1. ‹arithmetic› – introduction to operator overloading, 2. ‹relational› – implementing equality and ordering, 3. ‹access› – dereference, indexing and other access ops, 4. ‹convert› – conversion and assignment, 5. ‹files› – opening files, reading and writing strings 6. ‹streams› – from values to strings and back 7. ‹format› – overloading formatting operators Elementary exercises: 1. ‹cartesian› – complex numbers in algebraic form, 2. ‹force› – composing and scaling forces, 3. ‹forcefmt› – vectors redux, this time with IO Preparatory exercises: 1. ‹polar› – complex numbers in polar form, 2. ‹rational› – rational numbers with ordering, 3. ‹tmpfile› – an auto-erasing temporary file 4. ‹nibble› – a pointer-like class for sub-byte access, 5. ‹grep› – print matching lines 6. ‹fixnum› – more numbers, this time with a parser. Regular exercises: 1. ‹poly› – polynomials with addition and multiplication 2. ‹csv› – parse comma-separated numeric data 3. ‹set› – a set of integers with set operators, 4. ‹email› – a simplified RFC 822 parser 5. ‹json› – format a string → string map as JSON 6. ‹cpp› † – a very simple C preprocessor ## d. Demonstrations ### [‹arithmetic›] Operator overloading allows instances of classes to behave more like built-in types: it makes it possible for values of custom types to appear in expressions, as operands. Before we look at examples of how this looks, we need to define a class with some overloaded operators. For binary operators, it is customary to define them using a ‘friends trick’, which allows us to define a top-level function inside a class. As a very simple example, we will implement a class which represents integral values modulo 7 (this happens to be a finite field, with addition and multiplication). class gf7 /* C */ { int value; public: The constructor is trivial, it simply constructs a ‹gf7› instance from an integer. We mark it ‹explicit› to avoid surprising automatic conversions of integers into ‹gf7› instances. explicit gf7( int v ) : value( v % 7 ) {} /* C */ This is the ‘friend trick’ syntax for writing operators, and for binary operators, it is often the preferred one (because of its symmetry). The function is not really a part of the class in this case -- the trick is that we can write it here anyway. friend gf7 operator+( gf7 a, gf7 b ) /* C */ { return gf7( a.value + b.value ); // [a]₇ + [b]₇ = [a + b]₇ } For multiplication, we will use the more ‘orthodox‘ syntax, where the operator is a ‹const› method: the left operand is passed into the operator as ‹this›, the right operand is the argument. In general, operators-as-methods have one explicit argument less (unary operators take 0 arguments, binary take 1 argument). gf7 operator*( gf7 b ) const /* C */ { return gf7( value * b.value ); // [a]₇ * [b]₇ = [a * b]₇ } Values of type ‹gf7› cannot be directly compared (we did not define the required operators) -- instead, we provide this method to convert instances of ‹gf7› back into ‹int›'s. int to_int() const { return value; } /* C */ }; Operators can be also overloaded using ‘normal’ top-level functions, like this unary minus (which finds the additive inverse of the given element). Notice that we cannot access private fields (attributes) of the class here: gf7 operator-( gf7 x ) { return gf7( 7 - x.to_int() ); } /* C */ Now that we have defined the class and the operators, we can look at how is the result used. int main() /* demo */ /* C */ { gf7 a( 3 ), b( 4 ), c( 0 ), d( 5 ); Values ‹a›, ‹b› and so forth can be now directly used in arithmetic expressions, just as we wanted. gf7 x = a + b; /* C */ gf7 y = a * b; Let us check that the operations work as expected: assert( x.to_int() == c.to_int() ); /* [3]₇ + [4]₇ = [0]₇ */ /* C */ assert( y.to_int() == d.to_int() ); /* [3]₇ * [4]₇ = [5]₇ */ assert( (-a + a).to_int() == c.to_int() ); /* unary minus */ } That was arithmetic operator overloading. Let's now look at relational (ordering) operators, in ‹relational.cpp›. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹relational›] In this example, we will show relational operators, which are very similar to the arithmetic operators from previous example, except for their return types, which are ‹bool› values. The example which we will use in this case are sets of small natural numbers (1-64) with inclusion as the order. We will implement the full set of comparison operators, which is still required in C++17 but will no longer be needed in C++20 (with the spaceship operator). NB. Standard ordered containers like ‹std::set› and ‹std::map› require the operator less-than to define a «linear» order. The comparison operators in this example do «not» define a linear order. class set /* C */ { Each bit of the below number indicates the presence of the corresponding integer (the index of that bit) in the set. uint64_t bits; /* C */ public: Like before, we add an explicit constructor that takes an initial value. We use a «default argument» to say that the constructor can be used as a default constructor (without arguments), in which case it will create an empty ‹set›. explicit set( uint64_t to_set = 0 ) : bits( to_set ) {} /* C */ We also define a few methods to add and remove numbers from the set, to test for presence of a number and an emptiness check. void add( int i ) { bits |= 1ul << i; } /* C */ void del( int i ) { bits &= ~( 1ul << i ); } bool has( int i ) const { return bits & ( 1ul << i ); } bool empty() const { return !bits; } We will use the method syntax here, because it is slightly shorter. We start with (in)equality, which is very simple (the sets are equal when they have the same members): bool operator==( set b ) const { return bits == b.bits; } /* C */ bool operator!=( set b ) const { return bits != b.bits; } It will be quite useful to have set difference to implement the comparisons below, so let us also define that: set operator-( set b ) const { return set( bits & ~b.bits ); } /* C */ Since the non-strict comparison (ordering) operators are easier to implement, we will do that first. Set ‹b› is a superset of set ‹a› if all elements of ‹a› are also present in ‹b›, which is the same as the difference ‹a - b› being empty. bool operator<=( set b ) const { return ( *this - b ).empty(); } /* C */ bool operator>=( set b ) const { return ( b - *this ).empty(); } }; And finally the strict comparison operators, which are more conveniently written using top-level function syntax: bool operator<( set a, set b ) { return a <= b && a != b; } /* C */ bool operator>( set a, set b ) { return a >= b && a != b; } int main() /* demo */ /* C */ { set a; a.add( 1 ); a.add( 7 ); a.add( 13 ); set b; b.add( 1 ); b.add( 6 ); b.add( 13 ); In each pair of assertions below, the two expressions are not quite equivalent. Do you understand why? assert( a != b ); assert( !( a == b ) ); /* C */ assert( a == a ); assert( !( a != a ) ); The two sets are incomparable, i.e. neither is less than the other, but as shown above they are not equal either. assert( !( a < b ) ); assert( !( b < a ) ); /* C */ a.add( 6 ); // let's make ‹a› a superset of ‹b› /* C */ And check that the ordering operators work on ordered sets. assert( a > b ); assert( a >= b ); assert( a != b ); /* C */ assert( b < a ); assert( b <= a ); assert( b != a ); b.add( 7 ); /* let's make the sets equal */ /* C */ assert( a == b ); assert( a <= b ); assert( a >= b ); } That's all regarding relational operators, you will have a chance to implement your own in one of the exercises later. In the meantime, let us move on to ‘access’ operators: dereference, indirect access and indexing, in ‹access.cpp›. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹access›] This set of operators will be slightly more difficult. Surely, you remember the unary ‹*› operator from C, where it is used to dereference pointers. We haven't seen much of that in C++, except perhaps with iterators. We will now see how to implement a class which can be dereferenced like a pointer. We will also add indexing to the mix (like with plain C arrays, or ‹std::vector› or even ‹std::map›). Let us revisit the ‹zipper› class from last week. We will add indexing (relative to the focus), use a dereference operator to access the focus and we will not store integers, but points in a plane. Cue our favourite class, a ‹point›: struct point /* C */ { double x, y; point( double x, double y ) : x( x ), y( y ) {} We know equality comparison from previous examples. We will need it later on for writing test cases for ‹zipper›. bool operator==( point o ) const { return x == o.x && y == o.y; } /* C */ }; Now for the zipper. We will need to use ‹std::vector› to be able to index elements, but we will still use ‹left› and ‹right› like stacks. class zipper /* C */ { using stack = std::vector< point >; stack left, right; point focus; public: zipper( double x, double y ) : focus( x, y ) {} /* C */ Inserting points into the zipper. zipper &emplace_left( double x, double y ) /* C */ { left.emplace_back( x, y ); return *this; } zipper &emplace_right( double x, double y ) /* C */ { right.emplace_back( x, y ); return *this; } A helper method, so we don't repeat ourselves in the increment/decrement operators below. The trick is to pass the ‹left›/‹right› stacks by reference, since moving left and right is symmetric with regards to those (i.e. the code to move left is the same as to move right, with all occurrences of ‹left› and ‹right› swapped). void shift( stack &a, stack &b ) /* C */ { b.push_back( focus ); focus = a.back(); a.pop_back(); } First the pre-increment operators, i.e. ‹++x› and ‹--x›. Here, we use those operators in the manner of C pointer arithmetic (you may want to review that topic). zipper &operator++() { shift( right, left ); return *this; } /* C */ zipper &operator--() { shift( left, right ); return *this; } Now the post-increment: ‹x++› and ‹x--›. In this particular data structure, they are «expensive» and should «not» be used. They are here just to demonstrate the syntax and a common implementation technique. The difference is that post-increment needs to make a copy, since the «value» of the expression is the object «before» the increment/decrement was applied to it. zipper operator++( int ) { auto r = *this; ++*this; return r; } /* C */ zipper operator--( int ) { auto r = *this; --*this; return r; } The dereference (unary ‹*›) and indirect member access operators (mutable, i.e. non-‹const› overloads first, then the ‹const› overloads). Those operators allow us to treat ‹zipper› as if it was a pointer to a ‹point› instance (the one that is in focus). See ‹main› below to see how this works when used. point &operator*() { return focus; } /* C */ point *operator->() { return &focus; } const point &operator*() const { return focus; } /* C */ const point *operator->() const { return &focus; } And finally an indexing operator. We will not bother with the ‹const› version at this time: it would be certainly possible, but ugly and/or repetitive. point &operator[]( int i ) /* C */ { if ( i == 0 ) return focus; if ( i < 0 ) return left[ left.size() + i ]; if ( i > 0 ) return right[ right.size() - i ]; assert( false ); } }; int main() /* demo */ /* C */ { zipper z( 0, 0 ); // [0,0] /* C */ Notice the correspondence between ‹*x› and ‹x[ 0 ]› that we carried over from C pointers. assert( z[ 0 ] == point( 0, 0 ) ); /* C */ assert( *z == point( 0, 0 ) ); We will add a few items to the zipper, so that we can demonstrate the other operators. z.emplace_left( 1, 1 ); // (1,1) [0,0] /* C */ z.emplace_right( 2, 1 ); // (1,1) [0,0] (2,1) Check that the indexing operators behave as expected: negative indices give us items on the left and positive indices give us items on the right. assert( z[ -1 ] == point( 1, 1 ) ); /* C */ assert( z[ 1 ] == point( 2, 1 ) ); Let us check that indexing also works further out. z.emplace_left( 2, 2 ); // (1,1) (2,2) [0,0] (2,1) /* C */ assert( z[ -2 ] == point( 1, 1 ) ); assert( z[ -1 ] == point( 2, 2 ) ); The pre-decrement operator moves the focus of the zipper tho the left. Let's check that (and demonstrate the correspondence between ‹z[ 0 ]› and ‹*z› again, for a good measure). -- z; // (1,1) [2,2] (0,0) (2,1) /* C */ assert( z[ -1 ] == point( 1, 1 ) ); assert( z[ 0 ] == point( 2, 2 ) ); assert( *z == point( 2, 2 ) ); Finally the indirect access operators let us look at ‹x› and ‹y› of the focused point in a nice, succinct way. The syntax is the same that you used to access ‹struct› members via a pointer to the ‹struct› in C. assert( z->x == 2 ); /* C */ assert( z->y == 2 ); Move the zipper twice to the right and do a final check. ++ z; ++ z; // (1,1) (2,2) (0,0) [2,1] /* C */ assert( z->x == 2 ); assert( z->y == 1 ); } Next: quick introduction to exceptions, in ‹exceptions.cpp›. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹convert›] In this example, we will implement a class which behaves like a nullable reference to an integer. Taking a hint from Java, we will throw an exception when the user attempts to use a null reference. We first define the type which we will use to indicate an attempt to use an invalid (null) reference. class null_pointer_exception {}; /* C */ Now for the reference-like class itself. We need two basic ingredients to provide simple reference-like behaviours: we need to be able to (implicitly) convert a value of type ‹maybe_ref› to a value of type ‹int›. The other part is the ability to «assign» new values of type ‹int› to the referred-to variable, via instances of the class ‹maybe_ref›. class maybe_ref /* C */ { We hold a pointer internally, since real references in C++ cannot be null. int *_ptr = nullptr; /* C */ We will also define a helper (internal, private) method which checks whether the reference is valid. If the reference is null, it throws the above exception. void _check() const /* C */ { if ( !_ptr ) throw null_pointer_exception(); } public: /* C */ Constructors: the default-constructed ‹maybe_ref› instances are nulls, they have nowhere to point. Like real references in C++, we will allow ‹maybe_ref› to be initialized to point to an existing value. We take the argument by reference and convert that reference into a pointer by using the unary ‹&› operator, in order to store it in ‹_ptr›. maybe_ref() = default; /* C */ maybe_ref( int &i ) : _ptr( &i ) {} As mentioned earlier, we need to be able to (implicitly) convert ‹maybe_ref› instances into integers. The syntax to do that is ‹operator type›, without mentioning the return type (in this case, the return type is given by the name of the operator, i.e. ‹int› here). It is also possible to have reference conversion operators, by writing e.g. ‹operator const int &()›. However, we don't need one of those here because ‹int› is small, and we can't have both since that would cause a lot of ambiguity. operator int() const /* C */ { _check(); return *_ptr; } The final part is assignment: as you have learned in the lecture, ‹operator=› should return a reference to the assigned-to instance. It usually takes a ‹const› reference as an argument, but again we do not need to do that here. Below in the demo, we will point out where the assignment operator comes into play. maybe_ref &operator=( int v ) /* C */ { _check(); *_ptr = v; return *this; } }; int main() /* demo */ /* C */ { int i = 7; When initializing built-in references, we use ‹int &i_ref = i›. We can do the same with ‹maybe_ref›, but we need to keep in mind that this syntax calls the ‹maybe_ref( int )› constructor, «not» the assignment operator. maybe_ref i_ref = i; /* C */ Let us check that the reference behaves as expected. assert( i_ref == 7 ); /* uses conversion to ‹int› */ /* C */ i_ref = 3; /* uses the assignment operator */ assert( i_ref == 3 ); /* conversion to ‹int› again */ Check that the original variable has changed too. assert( i == 3 ); /* C */ Let's also check that null references behave as expected. bool caught = false; /* C */ maybe_ref null; Comparison will try to convert the reference to ‹int›, but that will fail in ‹_check› with an exception. try { assert( null == 7 ); } /* C */ catch ( const null_pointer_exception & ) { caught = true; } Make sure that the exception was thrown and caught. assert( caught ); /* C */ caught = false; Same but with assignment into the null referenc. try { null = 2; } /* C */ catch ( const null_pointer_exception & ) { caught = true; } assert( caught ); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹files›] This example will be brief: we will show how to open a file for reading and fetch a line of text. We will then write that line of text into a new file and read it back again to check that things worked. We will split up the example into functions for 2 reasons: first, to make it easier to follow, and second, to take advantage of RAII: the file streams will close the underlying resource when they are destroyed. In this case, that will be at the end of each function. std::string read( const char *file ) /* C */ { The default method of doing IO in C++ is through «streams». Reading files is done through a stream of type ‹std::ifstream›, which is short for «input file stream». The constructor of ‹ifstream› takes the name of the file to open. We will use a file given to us by the caller. std::ifstream f( file ); /* C */ The simplest method to read text from a file is using ‹std::getline›, which will fetch a single line at a time, into an ‹std::string›. We need to prepare the string in advance, since it is passed into ‹std::getline› as an output argument. std::string line; /* C */ The ‹std::getline› function returns a reference to the stream that was passed to it. Additionally, the stream can be converted to ‹bool› to find out whether everything is okay with it. If the reading fails for any reason, it will evaluate to ‹false›. The newline character is discarded. if ( !std::getline( f, line ) ) /* C */ In real code, we would of course want to handle errors, because opening files is something that can fail for a number of reasons. Here, we simply assume that everything worked. assert( false ); /* C */ return line; /* C */ } Next comes a function which demonstrates writing into files. void write( const char *file, std::string line ) /* C */ { To write data into a file, we can use ‹std::ofstream›, which is short for «output file stream». The output file is created if it does not exist. std::ofstream f( file ); /* C */ Writing into a file is typically done using operators for «formatted output». We will look at those in more detail in the next section. For now, all we need to know that writing an object into a stream is done like this: f << line; /* C */ We will also want to add the newline character that ‹getline› above chomped. We have two options: either use the ‹"\n"› string literal, or ‹std::endl› -- a so-called «stream manipulator» which sends a newline character and asks the stream to send the bytes to the operating system. Let's try the more idiomatic approach, with the manipulator: f << std::endl; /* C */ At this point, the file is automatically closed and any outstanding data is sent to the operating system. } /* C */ int main() /* demo */ /* C */ { We first use ‹read› to get the first line of this file. std::string line = read( "d5_files.cpp" ); /* C */ And we check that the line we got is what we expect. Remember the stripped newline. assert( line == "/* This example will be brief:" /* C */ " we will show how to open a file for" ); Now we write the line into another file. After you run this example, you can inspect ‹files.out› with an editor. It should contain a copy of the first line of this file. write( "d5_files.out", line ); /* C */ Finally, we use ‹read› again to read "file.out" back, and check that the same thing came back. std::string check = read( "d5_files.out" ); /* C */ assert( check == line ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹streams›] File streams are not the only kind of IO streams that are available in the standard library. There are 3 ‘special’ streams, called ‹std::cout›, ‹std::cerr› and ‹std::cin›. Those are not types, but rather global variables, and represent the standard output, the standard error output and the standard input of the program. However, the first two are instances of ‹std::ostream› and the third is an instance of ‹std::istream›. We don't know about class inheritance yet, but it is probably not a huge stretch to understand that instances of ‹std::ofstream› (output «file» stream) are also at the same time instances of ‹std::ostream› (general output stream). The same story holds for ‹std::ifstream› (input file stream) and ‹std::istream› (general input stream). There is another pair of classes: ‹std::ostringstream› and ‹std::istringstream›. Those streams are not attached to OS resources, but to instances of ‹std::string›: in other words, when you write to an ‹ostringstream›, the resulting bytes are not sent to the operating system, but are instead appended to the given string. Likewise, when you read from an ‹istringstream›, the data is not pulled from the operating system, but instead come from an ‹std::string›. Hopefully, you can see the correspondence between files (the content of which are byte sequences stored on disk) and strings (the content of which are byte sequences stored in RAM). In any case, string streams are ideal for playing around, because we can use the same tools as we always do: create some simple instances, apply operations and use ‹assert› to check that the results are what we expect. String-based streams are defined in the header ‹sstream›. Everything that we will do with string streams applies to other types of streams too (i.e. the 3 special streams mentioned earlier, and all file streams). Like in the previous example, we will split up the demonstration into a few sections, mainly to avoid confusion over variable names. We will first demonstrate reading from streams. We have already seen ‹std::getline›, so let's start with that. It is probably noteworthy that it works on any input stream, not just ‹std::ifstream›. void getline_1() /* C */ { std::istringstream istr( "a string\nwith 2 lines\n" ); /* C */ std::string s; assert( std::getline( istr, s ) ); /* C */ assert( s == "a string" ); assert( std::getline( istr, s ) ); assert( s == "with 2 lines" ); assert( !std::getline( istr, s ) ); assert( s.empty() ); } We can also override the delimiter character for ‹std::getline›, to extract delimited fields from input streams. void getline_2() /* C */ { std::istringstream istr( "colon:separated fields" ); std::string s; assert( std::getline( istr, s, ':' ) ); /* C */ assert( s == "colon" ); assert( std::getline( istr, s, ':' ) ); assert( s == "separated fields" ); assert( !std::getline( istr, s, ':' ) ); } So far so good. Our other option is so-called «formatted input». The standard library doesn't offer much in terms of ready-made overloads for such inputs: there is one for strings, which extracts individual words (like the ‹scanf› specifier ‹%s›, if you remember that from C, but the C++ version is actually safe and it is okay to use it). Then there is an instance for ‹char›, which extracts a single character (regardless of whether it is a whitespace character or not) and a bunch of overloads for various numeric types. void formatted_input() /* C */ { std::istringstream istr( "integer 123 float 3.1415 s t" ); std::string s, t; int i; float f; istr >> s; assert( s == "integer" ); /* C */ istr >> i; assert( i == 123 ); istr >> s; assert( s == "float" ); Notice that ‹float› numbers are not very exact. They are usually just 32 bits, which means 24 bits of precision, which is a bit less than 8 decimal digits. istr >> f; assert( std::fabs( f - 3.1415 ) < 1e-7 ); /* C */ The last thing we want to demonstrate with regards to the formatted input operators is that we can «chain» them. The values are taken from left to right (behind the scenes, this is achieved by the formatted input operator returning a reference to its left operand. istr >> s >> t; /* C */ assert( s == "s" && t == "t" ); When we reach the end of the stream (i.e. the end of the buffer, or of the file), the stream will indicate an error. A stream in error condition converts to ‹false› in a ‹bool› context. assert( !( istr >> s ) ); /* C */ } Output is actually quite a bit simpler than input. It is almost always reasonable to use formatted output, since strings are simply copied to the output without alterations. void formatted_output() /* C */ { std::ostringstream a, b, c; a << "hello world"; To read the buffer associated with an output string stream, we use its method ‹str›. Of course, this method is not available on other stream types: in those cases, the characters are written to files or to the terminal and we cannot access them through the stream anymore. assert( a.str() == "hello world" ); /* C */ Like with formatted input, output can be chained. b << 123 << " " << 3.1415; /* C */ assert( b.str() == "123 3.1415" ); When writing delimited values to an output stream, it is often desirable to only put the delimiter between items and not after each item: this is an endless source of headaches. Here is a trick to do it without too much typing: int i = 0; /* C */ for ( int v : { 1, 2, 3 } ) c << ( i++ ? ", " : "" ) << v; assert( c.str() == "1, 2, 3" ); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹format›] We have seen the basics of input and output, and that formatted input and output is realized using operators. Like many other operators in C++, those operators can be overloaded. We will show how that works in this example. We will revisit the ‹cartesian› class from last week, to represent complex numbers in algebraic form, i.e. as a sum of a real and an imaginary number. We do not care about arithmetic this time: we will only implement a constructor and the formatted input and output operators. We will, however, need equality so that we can write test cases. class cartesian /* C */ { double real, imag; public: We have seen default arguments before: those are used when no value is supplied by the caller. This also allows instances to be default-constructed. cartesian( double r = 0, double i = 0 ) : real( r ), imag( i ) {} /* C */ The comparison is fuzzy, due to the limited precision available in ‹double›. friend bool operator==( cartesian a, cartesian b ) /* C */ { return std::fabs( a.real - b.real ) < 1e-10 && std::fabs( a.imag - b.imag ) < 1e-10; } Now the formatted output, which is a little easier than the input. Since the first operand of this operator is «not» an instance of ‹cartesian›, the operator «cannot» be implemented as a method. It must either be a function outside the class, or use the ‘friend trick’. Since we will need to access private attributes in the operator, we will use the ‹friend› syntax here. The return type and the type of the first argument are pretty much given and are always the same. You could consider them part of the syntax. The second argument is an instance of our class (this would often be passed as a ‹const› reference). friend std::ostream &operator<<( std::ostream &o, cartesian c ) /* C */ { We will use ‹27.3±7.1*i› as the output format. We can use ‘simpler’ overloads of the ‹<<› operator to build up ours: this is a fairly common practice. We write to the ‹ostream› instance given to us in the argument. We must not forget to return that instance to our caller. o << c.real; /* C */ if ( c.imag >= 0 ) o << "+"; return o << c.imag << "*i"; } The input operator is similar. It gets a reference to an ‹std::istream› as an argument (and has to pass it along in the return value). The main difference is that the object into which we read the data must be passed as a non-constant (i.e. mutable) reference, since we need to change it. friend std::istream &operator>>( std::istream &i, cartesian &c ) /* C */ { Like above, we will build up our implementation from simpler overloads of the same operator (which all come from the standard library). The formatted input operators for numbers do not require that the number is followed by whitespace, but will stop at a character which can no longer be part of the number. A ‹+› or ‹-› character in the middle of the number qualifies. i >> c.real; /* C */ We will slightly abuse the flexibility of the formatted input operator for ‹double› values: it accepts numbers starting with an explicit ‹+› sign, hence we do not need to check the sign ourselves. Just read the imaginary part. i >> c.imag; /* C */ We do need to deal with the trailing ‹*i› though. char ch; /* C */ When formatted input fails, it should set a ‹failbit› in the input stream. This is how the ‹if ( stream >> value )› construct works. if ( !( i >> ch ) || ch != '*' || /* C */ !( i >> ch ) || ch != 'i' ) i.setstate( i.failbit ); And as mentioned above, we need to return a reference to the input stream. return i; /* C */ } }; /* C */ int main() /* demo */ /* C */ { std::ostringstream ostr; ostr << cartesian( 1, 1 ); We first check that the output behaves as we expected. assert( ostr.str() == "1+1*i" ); /* C */ We write a few more complex numbers into the stream, using operator chaining. ostr << " " << cartesian( 3, 0 ) << " " << cartesian( 1, -1 ) /* C */ << " " << cartesian( 0, 0 ); assert( ostr.str() == "1+1*i 3+0*i 1-1*i 0+0*i" ); /* C */ We now construct an input stream from the string which we created above, and check that the values can be read back. std::istringstream istr( ostr.str() ); /* C */ cartesian a, b, c; Let's read back the first number and check that the result makes sense. assert( istr >> a ); /* C */ assert( a == cartesian( 1, 1 ) ); We can also check that chaining works as expected, using the remaining numbers in the string. assert( istr >> a >> b >> c ); /* C */ assert( a == cartesian( 3, 0 ) ); /* C */ assert( b == cartesian( 1, -1 ) ); assert( c == cartesian( 0, 0 ) ); We can reset an ‹istringstream› by calling its ‹str› method with a new buffer. We want to demonstrate that trying to read an ill-formatted complex number will fail. std::istringstream bad1( "7+3*j" ); /* C */ assert( !( bad1 >> a ) ); std::istringstream bad2( "7" ); /* C */ assert( !( bad2 >> a ) ); } ## e. Elementary Exercises ### [‹cartesian›] In this exercise, we will implement complex numbers with addition, subtraction, unary minus and equality. The class should be called ‹complex› (do not mind the syntax highlight). The constructor should take 2 real numbers (the real and imaginary parts). class complex; /* C */ ### [‹force›] In this example, we will define a class that represents a (physical) force in 3D. Forces are «vectors» (in the mathematical sense): they can be added and multiplied by scalars (scalars are, in this case, real numbers). Forces can also be compared for equality (we will use fuzzy comparison because floating point computations are inexact). Hint: It may be useful to know that when overloading binary operators, the operands do not need to be of the same type. class force; /* C */ ### [‹forcefmt›] This week in the physics department, we will deal with formatting and parsing vectors (forces, just to avoid confusion with ‹std::vector›... for now). The class will be called ‹force›, and it should have a constructor which takes 3 values of type ‹double› and a default constructor which constructs a 0 vector. In addition to that, it should have a (fuzzy) comparison operator and formatting operators, both for input and for output. Use the following format: ‹[F_x F_y F_z]›, that is, a left square bracket, then the three components of the force separated by spaces, and a closing square bracket. Do not forget to set ‹failbit› in the input stream if the format does not match expectations. class force; /* C */ ## p. Preparatory Exercises ### [‹polar›] The first thing we will do is implement a simple class which represents complex numbers using their polar form. This form makes multiplication and division easier, so that is what we will do here (see also ‹cartesian.cpp› for definition of addition). • the constructor takes the modulus and the argument (angle) • add ‹abs› and ‹arg› methods to access the attributes • implement multiplication and division on ‹polar› • implement equality for ‹polar›; keep in mind that if the modulus is zero, the argument (angle) is irrelevant NB. The argument is «periodic»: either normalize it to fall within [0, 2π), or otherwise make sure that ‹polar( 1, x ) == polar( 1, x + 2π )›. The equality operator you implement should be tolerant of imprecision: use ‹std::fabs( x - y ) < 1e-10› instead of ‹x == y› when dealing with real numbers. class polar; /* reference implementation: 29 lines */ /* C */ ### [‹rational›] In this exercise, we will represent rational numbers (fractions) with addition and ordering. The constructor of ‹rat› should take the numerator and the denominator (in this order), which are both integers. It should be possible to compare ‹rat› instances for equality and inequality (in this exercise, it is enough to implement the less-than operator , i.e. ‹a < b›). NB. Recall how fractions with different denominators are compared (and added). Your implementation does not need to be very efficient, or work for very large numbers. class rat; /* reference implementation: 9 lines */ /* C */ ### [‹tmpfile›] We will implement a simple wrapper around ‹std::fstream› that will act as a temporary file. When the object is destroyed, use ‹std::remove› to unlink the file. Make sure the stream is closed before you unlink the file. The ‹tmpfile› class should have the following interface: • a constructor which takes the name of the file • method ‹write› which takes a string and replaces the content of the file with that string; this method should flush the data to the operating system (e.g. by closing the stream) • method ‹read› which returns the current content of the file • method ‹stream› which returns a reference to an instance of ‹std::fstream› (i.e. suitable for both reading and writing) Calling both ‹stream› and ‹write› on the same object is undefined behaviour. The ‹read› method should return all data sent to the file, including data written to ‹stream()› that was not yet flushed by the user. class tmpfile; /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹nibble›] In this exercise, we will implement a class that represents an array of nibbles (half-bytes) stored compactly, using a byte vector as backing storage. We will need 3 classes: one to represent reference-like objects: ‹nibble_ref›, another for pointer-like objects: ‹nibble_ptr› and finally the container to hold the nibbles: ‹nibble_vec›. NB. In this exercise, we will «not» consider ‹const›-ness of values stored in the vector.¹ The ‹nibble_ref› class needs to remember a reference or a pointer to the byte which contains the nibble that we refer to, and whether it is the upper or the lower nibble. With that information (which should be passed to it via a constructor), it needs to provide: • an «assignment operator» which takes an ‹uint8_t› as an argument, but only uses the lower half of that argument to overwrite the pointed-to nibble, • a «conversion operator» which allows implicit conversion of a ‹nibble_ref› to an ‹uint8_t›. class nibble_ref; /* reference implementation: 17 lines */ /* C */ The ‹nibble_ptr› class works as a pointer. «Dereferencing» a ‹nibble_ptr› should result in a ‹nibble_ref›. There is no indirect access, because the target (pointed-to) type does not have any fields. To make ‹nibble_ptr› more useful, it should also have: • a pre-increment operator, which shifts the pointer to the next nibble in memory. That is, if it points at a lower nibble, after ‹++x›, it should point to an «upper half» of the «same» byte, and after another ‹++x›, it should point to the «lower half» of the «next» byte, • an «equality comparison» operator, which checks whether two ‹nibble_ptr› instances point to the same place in memory. class nibble_ptr; /* reference implementation: 18 lines */ /* C */ And finally the ‹nibble_vec›: this class should provide 4 methods: • ‹push_back›, which adds a nibble at the end, • ‹begin›, which returns a ‹nibble_ptr› to the first stored nibble (lower half of the first byte), • ‹end›, which returns a ‹nibble_ptr› «past» the last stored nibble (i.e. the first nibble that is not in the container), and finally • indexing operator, which returns a ‹nibble_ref›. class nibble_vec; /* reference implementation: 16 lines */ /* C */ ¹ In particular, obtaining a pointer (e.g. by using ‹begin›) will allow you to change the value that it points to, even if the vector itself was marked ‹const›. We will deal with this issue properly toward the end of the semester. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹grep›] To practice working with IO streams a little, we will write a two simple functions which reads lines from an input stream, process them a little and possibly print them out or their part into an output stream. The ‹grep› function checks, for every line on the input, whether it matches a given ‹pattern› (i.e. the pattern is a substring of the line) and if it does (and only if it does) copies the line to the output stream. void grep( std::string pattern, std::istream &, std::ostream & ); /* C */ The other function to add is called ‹cut› and it will process the lines differently: it splits each line into fields separated by the character ‹delim› and only prints the column given by ‹col›. Unlike the ‹cut› program, index columns starting at 0. If there are not enough columns on a given line, print an empty line. void cut( char delim, int col, std::istream &, std::ostream & ); /* C */ ### [‹fixnum›] In this exercise, we will implement fixed-precision numbers, with 2 fractional digits and up to 6 integral digits (both decimal), i.e. numbers of the form ‘123456.78’. This is the class which we will use for indicating that parsing of the ‹fixnum› has failed (i.e. this class will be thrown as an exception in that case). class bad_format; /* C */ The ‹fixnum› class should provide following operations: addition, subtraction and multiplication. It should have «explicit» constructors which construct the number from an integer or from a string. The latter constructor should throw an exception if the string is ill-formed (it is okay to only handle positive numbers in string form). Finally, it should be possible to compare ‹fixnum› instances for equality. All operations should round toward zero, to the nearest representable number. class fixnum; /* reference implementation: 32 lines */ /* C */ ## r. Regular Exercises ### [‹poly›] Goal: implement polynomials with addition (easy) and multiplication (less easy). A polynomial is a term of the form ⟦7x⁴ + 0x³ + 0x² + 3x + x⁰⟧ -- i.e. a sum of non-negative integral powers of ⟦x⟧, with each power carrying a fixed (constant) coefficient. Adding two polynomials will simply give us a polynomial where coefficients are sums of the coefficients of the two addends. The case of multiplication is more complicated, because: • each term of the first polynomial has to be multiplied by each term of the second polynomial • some of those products give equal powers of ⟦x⟧ and hence their coefficients need to be summed For each polynomial, there is some ⟦n⟧, such that all powers higher than ⟦n⟧ have a zero coefficient. This is important when you want to store the polynomials in a computer. The default constructor of the class ‹poly› should generate a polynomial which has all coefficients set to 0. Besides addition and multiplication (which are implemented as operators), also implement equality and a method ‹set›, which takes an exponent (power of ⟦x⟧) and a coefficient, both integers. class poly; /* reference implementation is 45 lines */ /* C */ ### [‹csv›] In this exercise, we will deal with CSV files: we will implement a class called ‹csv› which will read data from an input stream and allow the user to access it using the indexing operator. The exception to throw in case of format error. class bad_format; /* C */ The constructor should accept a reference to ‹std::istream› and the expected number of columns. In the input, each line contains integers separated by value. The constructor should throw an instance of ‹bad_format› if the number of columns does not match. Additionally, if ‹x› is an instance of ‹csv›, then ‹x[ 3 ][ 1 ]› should return the value in the third row and first column. class csv; /* C */ ### [‹set›] In this exercise, we will implement a set of «arbitrary» integers, with the following operations: union using ‹|›, intersection using ‹&›, difference using ‹-› and inclusion using ‹<=›. Use efficient algorithms for the operations (check out what's available in the standard header ‹algorithm›). Provide methods ‹add› and ‹has› to add elements and test their presence. class set; /* reference implementation: 36 lines */ /* C */ ### [‹json›] You are given a single-level string → string dictionary. Turn it into a single string, using JSON as the format. Take care to escape special characters – at least double quote and the escape character (backslash) itself. In JSON, key order is not important – emit them in iteration (alphabetic) order. Put a single space after each ‘element’: after the opening brace, after colons and after commas, except if the input is empty, in which case the output should be just ‹{}›. using str_dict = std::map< std::string, std::string >; /* C */ std::string to_json( const str_dict &dict ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹cpp›] Implement a (very simplified) C preprocessor which supports ‹#include "foo"› (without a search path, working directory only), ‹#define› without a value, ‹#undef›, ‹#ifdef› and ‹#endif›. The input is provided in a file, but the output should be returned as a string. PS: Do not include line and filename information that ‹cpp› normally adds to files. std::string cpp( const std::string &filename ); /* C */ If you run this program with a parameter, it'll preprocess that file and print the result to stdout. Feel free to experiment. int main( int argc, const char **argv ) /* C */ { if ( argc >= 2 ) std::cout << cpp( argv[ 1 ] ); else { std::string actual_1 = cpp( "zz.preproc_1.txt" ), expect_1 = "included foo\n" "included bar\n" "xoo\n" "foo\n", actual_2 = cpp( "zz.preproc_2.txt" ), expect_2 = "included bar\n" "included baz\n" "included bar\n"; assert( actual_1 == expect_1 ); /* C */ assert( actual_2 == expect_2 ); } return 0; /* C */ } # Exceptions and RAII Demonstrations: 1. ‹exceptions› – throwing and catching exceptions 2. ‹stdexcept› – the standard exception hierarchy 3. ‹semaphore› – automatic management of finite resources 4. ‹swarm› – keeping the swarm under control Elementary exercises: 1. ‹default› – read a number or return a default value 2. ‹counter› – count the number of instances of a class 3. ‹coffee› – a simple model of a coffee machine Preparatory exercises: 1. ‹fd› – POSIX file descriptors 2. ‹loan› – database-style transactions with resources 3. ‹library› – borrowing books 4. ‹parse› – a simple parser which throws exceptions 5. ‹invest› – we further stretch the banking story 6. ‹linear› – linear equations, with some exceptions Regular exercises: 1. ‹printing› – printing with a monthly budget 2. ‹bsearch› – a key-value vector which throws on failure 3. ‹enzyme› – cellular chemistry with RAII 4. ‹tinyvec› † – a vector in a fixed memory buffer 5. ‹lock› – a movable mutual exclusion token 6. ‹bounded› – a bounded queue that throws when full ## d. Demonstrations ### [‹exceptions›] Exceptions are, as their name suggests, a mechanism for handling unexpected or otherwise «exceptional» circumstances, typically error conditions. A canonic example would be trying to open a file which does not exist, trying to allocate memory when there is no free memory left and the like. Another common circumstance would be errors during processing user input: bad format, unexpected switches and so on. NB. Do «not» use exceptions for ‘normal’ control flow, e.g. for terminating loops. That is a «really» bad idea (even though ‹try› blocks are cheap, throwing exceptions is very expensive). This example will be somewhat banal. We start by creating a class which has a global counter of instances attached to it: i.e. the value of ‹counter› tells us how many instances of ‹counted› exist at any given time. Fair warning, do not do this at home. int counter = 0; /* C */ struct counted /* C */ { counted() { ++ counter; } ~counted() { -- counter; } }; A few functions which throw exceptions and/or create instances of the ‹counted› class above. Notice that a ‹throw› statement immediately stops the execution and propagates up the call stack until it hits a ‹try› block (shown in the ‹main› function below). The same applies to a function call which hits an exception: the calling function is interrupted immediately. int f() { counted x; return 7; } /* C */ int g() { counted x; throw std::bad_alloc(); assert( 0 ); } int h() { throw std::runtime_error( "h" ); } int i() { counted x; g(); assert( 0 ); } int main() /* demo */ /* C */ { bool caught = false; A ‹try› block allows us to detect that an exception was thrown and react, based on the type and attributes of the exception. Otherwise, it is a regular block with associated scope, and behaves normally. try /* C */ { counted x; assert( counter == 1 ); f(); assert( counter == 1 ); } One or more ‹catch› blocks can be attached to a ‹try› block: those describe what to do in case an exception of a matching type is thrown in one of the statements of the ‹try› block. The ‹catch› clause behaves like a prototype of a single-argument function -- if it could be ‘called’ with the thrown exception as an argument, it is executed to «handle» the exception. This particular ‹catch› block is never executed, because nothing in the associated ‹try› block above throws a matching exception (or rather, any exception at all): catch ( const std::bad_alloc & ) { assert( false ); } /* C */ The ‹counted› instance ‹x› above went out of scope: assert( counter == 0 ); /* C */ Let's write another ‹try› block. This time, the ‹i› call in the ‹try› block throws, indirectly (via ‹g›) an exception of type ‹std::bad_alloc›. try { i(); } /* C */ To demonstrate how ‹catch› blocks are selected, we will first add one for ‹std::runtime_error›, which will not trigger (the ‘prototype’ does not match the exception type that was thrown): catch ( const std::runtime_error & ) { assert( false ); } /* C */ As mentioned above, each ‹try› block can have multiple ‹catch› blocks, so let's add another one, this time for the ‹bad_alloc› that is actually thrown. If the ‹catch› matches the exception type, it is executed and propagation of the exception is stopped: it is now handled and execution continues normally after the end of the ‹catch› sequence. catch ( const std::bad_alloc & ) { caught = true; } /* C */ Execution continues here. We check that the ‹catch› block was actually executed: assert( caught ); /* C */ assert( counter == 0 ); // no ‹counted› instances were leaked } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹stdexcept›] It is possible to sub-class standard exception classes. For most uses, ‹std::runtime_error› is the most appropriate base class. class custom_exception : public std::runtime_error /* C */ { public: custom_exception() : std::runtime_error( "custom" ) {} }; This demo simply demonstrates some of the standard exception types (i.e. those that are part of the standard library, and which are thrown by standard functions or methods; as long as those methods or functions are not too arcane). int main() /* demo */ /* C */ { try { throw custom_exception(); assert( false ); } As per standard rules, it's possible to catch exceptions of derived classes (of course including user-defined types) via a ‹catch› clause which accepts a reference to a superclass. catch ( const std::exception & ) {} /* C */ try /* C */ { std::vector x{ 1, 2 }; Attempting out-of-bounds access through ‹at› gives ‹std::out_of_range› x.at( 7 ); /* C */ assert( false ); } catch ( const std::out_of_range & ) {} try /* C */ { If the string passed to ‹stoi› is not a number, we get back an exception of type ‹std::invalid_argument›. std::stoi( "foo" ); /* C */ assert( false ); } catch ( const std::invalid_argument & ) {} try /* C */ { If an integer is too big to fit the result type, ‹stoi› throws ‹std::out_of_range›. std::stoi( "123456123456123456" ); /* C */ assert( false ); } catch ( const std::out_of_range & ) {} try /* C */ { System-interfacing functions may throw ‹std::system_error›. Here, for instance, trying to detach a thread which was not started. std::thread().detach(); /* C */ assert( false ); } catch ( const std::system_error & ) {} try /* C */ { Throwing a ‹system_error› is the appropriate reaction when dealing with a failure of a POSIX function which sets ‹errno›. int fd = ::open( "/does/not/exist", O_RDONLY ); /* C */ if ( fd < 0 ) throw std::system_error( errno, std::system_category(), "opening /does/not/exist" ); assert( false ); } catch ( const std::system_error & ) {} try /* C */ { Passing a size that is more than ‹max_size()› when constructing or resizing an ‹std::string› or an ‹std::vector› gives us back an ‹std::length_error›. Note that the -1 turns into a really big number in this context. std::string x( -1, 'x' ); /* C */ assert( false ); } catch ( const std::length_error & ) {} try /* C */ { std::bitset< 128 > x; x[ 100 ] = true; Trying to convert an ‹std::bitset› to an integer type may throw ‹std::overflow_error›, if there are bits set that do not fit into the target integer type. x.to_ulong(); /* C */ assert( false ); } catch ( const std::overflow_error & ) {} } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹semaphore›] In this demo, we will implement a simple semaphore. A semaphore is a device which guards a resource of which there are multiple instances, but the number of instances is limited. It is a slight generalization of a mutex (which guards a singleton resource). Internally, semaphore simply counts the number of clients who hold the resource and refuses further requests if the maximum is reached. In a multi-threaded program, semaphores would typically block (wait for a slot to become available) instead of refusing. In a single-threaded program (which is what we are going to use for a demonstration), this would not work. Hence our ‹get› method returns a ‹bool›, indicating whether acquisition of the lock succeeded. class semaphore /* C */ { int _available; public: When a semaphore is constructed, we need to know how many instances of the resource are available. explicit semaphore( int max ) : _available( max ) {} /* C */ Classes which represent resource managers (in this case ‘things that can be locked’ as opposed to ‘locks held’) have some tough choices to make. If they are impossible to copy/move/assign, users will find that they must not appear as attributes in their classes, lest those too become un-copyable (and un-movable) by default. However, this is how the standard library deals with the problem, see ‹std::mutex› or ‹std::condition_variable›. While it is the safest option, it is also the most annoying. Nonetheless, we will do the same. semaphore( const semaphore & ) = delete; /* C */ semaphore &operator=( const semaphore & ) = delete; We allow would-be lock holders to query the number of resource instances currently available. Perhaps if none are left, they can make do without one, or they can perform some other activity in the hopes that the resource becomes available later. int available() const /* C */ { return _available; } Finally, what follows is the ‘low-level’ interface to the semaphore. It is completely unsafe, and it is inadvisable to use it directly, other than perhaps in special circumstances. This being C++, such interfaces are commonly made available. Again see ‹std::mutex› for an example. However, it would also be an option to be strict about it, make the following 2 methods private, and declare the RAII class defined below, ‹semaphore_lock›, to be a friend of this one. bool get() /* C */ { if ( _available > 0 ) return _available --; else return false; } void put() /* C */ { ++ _available; } }; We will want to write a RAII ‘lock holder’ class. However, since ‹get› above might fail, we need a way to indicate the failure in the RAII class as well. But constructors don't return values: it is therefore a reasonable choice to throw an exception. It is reasonable as long as we don't expect the failure to be a common scenario. class resource_exhausted : public std::runtime_error /* C */ { public: resource_exhausted() : std::runtime_error( "semaphore full" ) {} }; Now the RAII class itself. It will need to hold a reference to the semaphore for which it holds a lock (good thing the semaphore is not movable, so we don't have to think about its address changing). Of course, it must not be possible to make a copy of the resource class: we cannot duplicate the resource, which is a lock being held. However, it does make sense to move the lock to a new owner, if the client so wishes. Hence, both a move constructor and move assignment are appropriate. class semaphore_lock /* C */ { semaphore *_sem = nullptr; public: To construct a semaphore lock, we understandably need a reference to the semaphore which we wish to lock. You might be wondering why the attribute is a pointer and the argument is a reference. The main difference between references and pointers (except the syntactic sugar) is that references cannot be null. In a correct program, all references always refer to valid objects. It does not make sense to construct a semaphore_lock which does not lock anything. Hence the reference. Why the pointer in the attributes? That will become clear shortly. Before we move on, notice that, as promised, we throw an exception if the locking fails. Hence, no ‹noexcept› on this constructor. semaphore_lock( semaphore &s ) : _sem( &s ) /* C */ { if ( !_sem->get() ) throw resource_exhausted(); } As outlined above, semaphore locks cannot be copied or assigned. Let's make that explicit. semaphore_lock( const semaphore_lock & ) = delete; /* C */ semaphore_lock &operator=( const semaphore_lock & ) = delete; The new object (the one initialized by the move constructor) is quite unremarkable. The interesting part is what happens to the ‘old’ (source) instance: we need to make sure that when it is destroyed, it does not release the resource (i.e. the lock held) – the ownership of that has been transferred to the new instance. This is where the pointer comes in handy: we can assign ‹nullptr› to the pointer held by the source instance. Then we just need to be careful when we release the resource (in the destructor, but also in the move assignment operator) – we must first check whether the pointer is valid. Also notice the ‹noexcept› qualifier: even though the ‘normal’ constructor throws, we are not trying to obtain a new resource here, and there is nothing in the constructor that might fail. This is good, because move constructors, as a general rule, should not throw. semaphore_lock( semaphore_lock &&src ) noexcept /* C */ : _sem( src._sem ) { src._sem = nullptr; } We now define a helper method, ‹release›, which frees up (releases) the resource held by this instance. It will do this by calling ‹put› on the semaphore. However, if the semaphore is null, we do nothing: the instance has been moved from, and no longer owns any resources. Why the helper method? Two reasons: 1. it will be useful in both the move assignment operator and in the destructor, 2. the client might need to release the resource before the instance goes out of scope or is otherwise destroyed ‘naturally’ (compare ‹std::fstream::close()›). void release() noexcept /* C */ { if ( _sem ) _sem->put(); } Armed with ‹release›, writing both the move assignment and the destructor is easy. The move assignment is also ‹noexcept›, which is semaphore_lock &operator=( semaphore_lock &&src ) noexcept /* C */ { First release the resource held by the current instance. We cannot hold both the old and the new resource at the same time. release(); /* C */ Now we reset our ‹_sem› pointer and update the ‹src› instance – the resource is now in our ownership. _sem = src._sem; /* C */ src._sem = nullptr; return *this; } ~semaphore_lock() noexcept /* C */ { release(); } }; int main() /* demo */ /* C */ { semaphore sem( 3 ); sem.get(); semaphore_lock l1( sem ); bool l4_made = false; try /* C */ { semaphore_lock l2( sem ); assert( sem.available() == 0 ); semaphore_lock l3 = std::move( l2 ); assert( sem.available() == 0 ); semaphore_lock l4 = std::move( l1 ); assert( sem.available() == 0 ); l4_made = true; semaphore_lock l5( sem ); assert( false ); } catch ( const resource_exhausted & ) {} assert( l4_made ); /* C */ assert( sem.available() == 2 ); // clang-tidy: -clang-analyzer-deadcode.DeadStores /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹swarm›] TBD. Create overlords which create a resource and non-overlords which consume it. Enforce the balance by throwing an exception on exhaustion. class swarm; /* C */ class unit /* C */ { swarm &owner; }; class overlord : unit /* C */ { }; class zergling : unit /* C */ { }; class swarm /* C */ { int _control = 3; int _resource = 200; public: overlord spawn_overlord(); zergling spawn_zergling(); }; int main() /* demo */ /* C */ { swarm s; std::vector< zergling > zerglings; /* C */ std::vector< overlord > overlords; zerglings.emplace_back( s ); /* C */ } ## e. Elementary Exercises ### [‹default›] Write a function ‹stoi_or› which takes a string and an ‹int›. If the string can be parsed using ‹std::stoi›, return the result of ‹stoi›, otherwise return the ‘default’ value from the second argument. ### [‹counter›] static int counter = 0; /* C */ Add constructors and a destructor to ‹counted› in such a way that ‹counter› above always corresponds to the number of instances of ‹counted› that exist at any given time. struct counted; /* C */ ### [‹coffee›] Implement a coffee machine which gives out a token when the order is placed and takes the token back when it is done… at most one order can be in progress. Throw this when the machine is already busy making coffee. class busy {}; /* C */ And this when trying to use a default-constructed or already-used token. class invalid {}; /* C */ Fill in the two classes. Besides constructors and assignment operators, add methods ‹make› and ‹fetch› to ‹machine›, to create and redeem tokens respectively. class machine; /* C */ class token; ### [‹lock›] TBD lock a resource, with ownership transfer but no copy ## p. Preparatory Exercises ### [‹fd›] In POSIX systems, opening a file or a file-like resource gives us a «file descriptor», a small number that can be passed to system calls such as ‹read› or ‹write›. The descriptor must be closed when it is no longer needed, by calling ‹close› on it exactly once (it is important not to close the same descriptor twice). Write a class which safely wraps a file descriptor so that we can't accidentally lose it or close it twice. It should be possible to move-construct and move-assign file descriptors. A new valid descriptor can be created in 2 ways: by calling ‹fd::open( "file", flags )› or ‹fd::dup( raw_fd )› where ‹flags› and ‹raw_fd› are both ‹int›. Use POSIX functions ‹open› and ‹dup› to implement this. Run ‹man 2 open› and ‹man 2 dup› on ‹aisa› for details about these POSIX functions. Add methods ‹read› and ‹write› to the ‹fd› class, the first will simply take an integer, read the given number of bytes and return an ‹std::string›. The latter will take an ‹std::string› and write it into the descriptor. Again see ‹man 2 read› and ‹man 2 write› on ‹aisa› for advice. If ‹open›, ‹read› or ‹write› fails, throw ‹std::system_error›. Attempting to call ‹read› or ‹write› on an invalid descriptor (one that was default-constructed or already closed) should throw ‹std::invalid_argument›. ### [‹loan›] Let us revisit the bank account story from first week. We will have 2 classes this time: an ‹account›, which has the usual methods: ‹deposit›, ‹withdraw›, ‹balance›; to simplify things, we will only add a default constructor, which sets the initial balance to 0. The other class will be called ‹loan›, and its constructor will take a reference to an ‹account› and the amount loaned (an ‹int›). Constructing a ‹loan› object will deposit the loaned amount to the referenced account. It will also have a method called ‹repay› which takes an integer, which withdraws the given amount from the associated account and reduces the amount owed by the same sum. Attempting to repay more than is owed should throw ‹std::out_of_range›. Make sure that we can't accidentally destroy a ‹loan› without repaying it first. Does it make sense to make a copy of a ‹loan›? How about move? And assignment? class account; /* C */ class loan; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹library›] A very simple library model: patrons can borrow books and borrowed books can be moved around, and must be eventually returned. The library should have the following methods: • ‹add_book›, which creates a book record based on 2 arguments – the title (a string) and the number of copies (an integer) of the book – and returns a suitable object (a handle) to represent that book, • ‹add_patron› which creates a patron, given a name (a string), and again returns a suitable object to represent the patron. It should be possible to call ‹borrow› on objects which represent patrons, passing either a reference to a library or another patron as the first argument, and the book handle as a second argument. It returns ‹true› if the borrowing was a success, or ‹false› otherwise (no copies were available). If a patron is destroyed, all books in their possession return to the library. Destroying a book handle does nothing. class library; /* C */ Finally, the class ‹loan› holds information about a loan. Both ‹library› and the patron object get a method ‹give› which returns a ‹loan› object associated with the book passed to it, and ‹take›, which accepts a ‹loan› object and takes ownership of the associated book. If ‹give› is called on an object which does not have a copy of the requested book, return an invalid (empty) ‹loan› object. While a book is held in a ‹loan› instance, it is not in the possession of any of the objects, but it is checked out from the library. If the ‹loan› object is destroyed without being ‹take›n by anyone, the book returns to the library. class loan; /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹parse›] Write a simple parser for an assembly-like language with one instruction per line (each taking 2 operands, separated by spaces, where the first is always a register and the second is either a register or an ‘immediate’ number). The opcodes (instructions) are: ‹add›, ‹mul›, ‹jnz›, the registers are ‹rax›, ‹rbx› and ‹rcx›. The result is a vector of ‹instruction› instances (see below). Set ‹r_2› to ‹reg::immediate› if the second operand is a number. If the input does not conform to the expected format, throw ‹no_parse›, which includes a line number with the first erroneous instruction and the kind of error (see ‹enum error›), as public attributes ‹line› and ‹type›, respectively. If multiple errors appear on the same line, give the one that comes first in the definition of ‹error›. You can add attributes or methods to the structures below, but do not change the enumerations. enum class opcode { add, mul, jnz }; /* C */ enum class reg { rax, rbx, rcx, immediate }; enum class error { bad_opcode, bad_register, bad_immediate, bad_structure }; struct instruction /* C */ { opcode op; reg r_1, r_2; int32_t immediate; }; struct no_parse /* C */ { int line; error type; }; std::vector< instruction > parse( const std::string & ); /* C */ #include /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹invest›] We will revisit (again) our familiar example of a bank account. This time, we add exceptions to the story: withdrawals that would exceed the overdraft limit will throw. We will also add a class dual to ‹loan› from the last time: an ‹investment›, which will deduct money from an account upon construction, accrue interest, and upon destruction, deposit the money into the original account. We will use this class as the exception type. It is okay to keep it empty. class insufficient_funds; /* C */ First the ‹account› class, which has the usual methods: ‹balance›, ‹deposit› and ‹withdraw›. The starting balance is 0. The balance must be non-negative at all times: an attempt to withdraw more money than available should throw an exception of type ‹insufficient_funds›. class account; /* reference implementation: 13 lines */ /* C */ And finally the class ‹investment›, which has a three-parameter constructor: it takes a reference to an ‹account›, the sum to invest and a yearly interest rate (in percent, as an integer). Upon construction, it must withdraw the sum from the account, and upon destruction, deposit the original sum plus the interest. The method ‹next_year› should update the accrued interest. class investment; /* reference implementation: 15 lines */ /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹linear›] Write a solver for linear equations in 2 variables. The interface will be a little unconventional: overload operators ‹+›, ‹*› and ‹==› and define global constants ‹x› and ‹y› of suitable types, so that it is possible to write the equations as shown in ‹main› below. Note that the return type of ‹==› does not have to be bool. It can be any type you like, including of course custom types. For ‹solve›, I would suggest looking up Cramer's rule. ref: class ‹eqn› 25 lines, ‹solve› 8 lines, ‹x› and ‹y› 2 lines If the system has no solution, throw an exception of type ‹no_solution›. Derive it from ‹std::exception›. ## r. Regular Exercises ### [‹printing›] Jobs need resources (printing credits, where 1 page = 1 credit) which must be reserved when the job is queued, but are only consumed at actual printing time; jobs can be moved between queues (printers) by the system, and jobs that are still in the queue can be aborted. The class ‹job› represents a document to be printed, along with resources that have already been earmarked for its printing. • The constructor should take a numeric identifier, the name of the user who owns the job, and the number of pages (= credits allocated for the job), • method ‹owner› should return the name of the owner, • method ‹page_count› should return the number of pages. class job; /* C */ A single ‹queue› instance represents a printer. It should have the following methods: • ‹dequeue›: consume (print) the «oldest» job in the queue and return its ‹id›, • ‹enqueue›: add a job to the queue, • ‹release( id )›: remove the job given by ‹id› from the queue and return it to the caller, • ‹page_count›: number of pages in the queue. You can assume that oldest job has the lowest ‹id›. class queue; /* C */ class insufficient_credit {}; /* C */ class print_sys /* C */ { int _last_id = 0; std::vector< queue > _printers; /* C */ std::map< std::string, int > _credit; std::map< int, int > _jobs; public: /* C */ int print( std::string user, int pages ) { if ( _credit[ user ] < pages ) throw insufficient_credit(); assert( !_printers.empty() ); /* C */ int qid = 0; for ( size_t i = 0; i < _printers.size(); ++i ) /* C */ if ( _printers[ i ].page_count() < _printers[ qid ].page_count() ) qid = i; int jid = _last_id ++; /* C */ _printers[ qid ].enqueue( job( jid, user, pages ) ); _jobs[ jid ] = qid; _credit[ user ] -= pages; return jid; } bool abort( int jid ) /* C */ { if ( !_jobs.count( jid ) ) return false; auto &queue = _printers[ _jobs[ jid ] ]; /* C */ job j = queue.release( jid ); _credit[ j.owner() ] += j.page_count(); _jobs.erase( jid ); return true; } int add_printer() /* C */ { _printers.emplace_back(); return int( _printers.size() ) - 1; } void add_credit( std::string user, int n ) /* C */ { _credit[ user ] += n; } void printing_done( int qid ) /* C */ { if ( _printers[ qid ].page_count() > 0 ) { int id = _printers[ qid ].dequeue(); assert( _jobs.count( id ) ); assert( _jobs[ id ] == qid ); _jobs.erase( id ); } } }; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹bsearch›] Let's revisit the perennial favourite: binary search. We will implement a container similar to ‹std::map›. Let's assume that it'll be used in a search-heavy scenario, so the cost of insertion is much less important than the cost of lookup. A good candidate, then, would be a sorted vector (lookup is logarithmic like with a tree, but the data is stored much more compactly). Implement at least ‹emplace›, an indexing operator, and ‹at›, with semantics familiar from ‹std::map›, except ‹emplace› should simply return a ‹bool› (we will not write iterators). Finally, since we still don't know how to write generic classes, use strings for keys and ‹token› instances for values. class token /* C */ { int _value; public: token( int i ) : _value( i ) {} token( const token & ) = delete; token &operator=( const token & ) = delete; token( token && ) = default; token &operator=( token && ) = default; token &operator=( int v ) { _value = v; return *this; } bool operator==( int v ) const { return _value == v; } }; class flat_map; /* C */ ### [‹enzyme›] TBD. Reactions tie up enzymes, which return to the pool after the reaction is done. Different reactions need different sets of enzymes present, and a given enzyme cannot be used by more than one reaction at a time. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹tinyvec›] † Implement ‹tiny_vector›, a class which works like a vector, but instead of allocating memory dynamically, it uses a fixed-size buffer (32 bytes) which is part of the object itself (use e.g. an ‹std::array› of bytes). Like earlier, we will use ‹token› as the value type. Provide the following methods: • ‹insert› (take an index and an rvalue reference), • ‹erase› (take an index), • ‹back› and ‹front›, with appropriate return types. In this exercise (unlike in most others), you are allowed to use ‹reinterpret_cast›. class token /* C */ { int _value; bool _robbed = false; public: static int _count; token( int i ) : _value( i ) { ++ _count; } /* C */ ~token() { if ( !_robbed ) -- _count; } token( const token & ) = delete; /* C */ token( token &&o ) : _value( o._value ) { o._robbed = true; } token &operator=( const token & ) = delete; /* C */ token &operator=( token &&o ) { if ( !_robbed && o._robbed ) -- _count; _value = o._value; _robbed = o._robbed; o._robbed = true; return *this; } token &operator=( int v ) /* C */ { _value = v; _robbed = false; return *this; } bool operator==( int v ) const /* C */ { assert( !_robbed ); return _value == v; } }; Throw this if ‹insert› is attempted but the element wouldn't fit into the buffer. class insufficient_space {}; /* C */ Hint: Use ‹uninitialized_*› and ‹destroy(_at)› functions from the ‹memory› header. class tiny_vector; /* C */ int token::_count = 0; /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹lock›] Implement class ‹lock› which holds a mutex locked as long as it exists. The ‹lock› instance can be moved around. For simplicity, the ‹mutex› itself is immovable. class mutex /* C */ { bool _locked = false; public: ~mutex() { assert( !_locked ); } mutex() = default; /* C */ mutex( const mutex & ) = delete; mutex( mutex && ) = delete; mutex &operator=( const mutex & ) = delete; mutex &operator=( mutex && ) = delete; void lock() { assert( !_locked ); _locked = true; } /* C */ void unlock() { assert( _locked ); _locked = false; } bool locked() const { return _locked; } }; class lock; /* C */ ### [‹bounded›] Implement a simple FIFO of integers. The constructor takes the maximum number of items in the queue as its sole argument. Add methods ‹push›, ‹pop›, ‹full›, ‹empty› and ‹next› (the last of which simply returns the next value, without changing anything). If the queue is full, ‹push› should throw ‹insufficient_space›. Try to make the implementation efficient (i.e. no ‹deque›). class insufficient_space; /* C */ class bounded_queue; # Memory and Smart Pointers Before you dig into the demonstrations and exercises, do not forget to read the extended introduction below. That said, the units for this week are, starting with demonstrations: 1. ‹queue› – a queue with stable references 2. ‹finexp› – like regexps but finite 3. ‹expr› – expressions with operators and shared pointers 4. ‹family› – genealogy with weak pointers Elementary exercises: 1. ‹dynarray› – a simple array with a dynamic size 2. ‹list› – a simple linked list with minimal interface Preparatory exercises: 1. ‹unrolled› – a linked list of arrays 2. ‹bittrie› – bitwise tries (radix trees) 3. ‹solid› – efficient storage of optional data 4. ‹chartrie› – binary tree for holding string keys 5. ‹bdd› – binary decision diagrams 6. ‹rope› – a string-like structure with cheap concatenation Regular exercises: 1. ‹circular› – a singly-linked circular list 2. ‹zipper› – implementing zipper as a linked list 3. ‹segment› – a binary tree of disjoint intervals 4. ‹diff› – automatic differentiation 5. ‹critbit› – more efficient version of binary tries 6. ‹refcnt› † – implement a simple reference-counted heap ## A. Exclusive Ownership So far, we have managed to almost entirely avoid thinking about memory management: standard containers manage memory behind the scenes. We sometimes had to think about «copies» (or rather avoiding them), because containers could carry a lot of memory around and copying all that memory without a good reason is rather wasteful (this is why we often pass arguments as ‹const› references and not as values). This week, we will look more closely at how memory management works and what we can do when standard containers are inadequate to deal with a given problem. In particular, we will look at building our own pointer-based data structures and how we can retain automatic memory management in those cases using ‹std::unique_ptr›. XXX ## B. Shared Ownership While ‹unique_ptr› is very useful and efficient, it only works in cases where the ownership structure is clear, and a given object has a single owner. When ownership of a single object is shared by multiple entities (objects, running functions or otherwise), we cannot use ‹unique_ptr›. To be slightly more explicit: shared ownership only arises when the lifetime of the objects sharing ownership is «not» tied to each other. If A owns B and A and B both need references to C, we can assign the ownership of C to object A: since it also owns B, it must live at least as long as B and hence there ownership is not actually shared. However, if A needs to be able to transfer ownership of B to some other, unrelated object while still retaining a reference to C, then C will indeed be in shared ownership: either A or B may expire first, and hence neither can safely destroy the shared instance of C to which they both keep references. In many modern languages, this problem is solved by a «garbage collector», but alas, C++ does not have one. Of course, it is usually better to design data structures in a way that allows for clear, 1:1 ownership structure. Unfortunately, this is not always easy, and sometimes it is not the most efficient solution either. Specifically, when dealing with large immutable (or persistent, in the functional programming sense) data structures, shared ownership can save considerable amount of memory, without introducing any ill side-effects, by only storing common sub-structures once, instead of cloning them. Of course, there are also cases where «shared mutable state» is the most efficient solution to a problem. ## d. Demonstrations ### [‹queue›] In this example, we will demonstrate the use of ‹std::unique_ptr›, which is an RAII class for holding (owning) values dynamically allocated from the heap. We will implement a simple one-way, non-indexable queue. We will require that it is possible to erase elements from the middle in O(1), without invalidating any other iterators. The standard containers which could fit: • ‹std::deque› fails the erase in the middle requirement, • ‹std::forward_list› does not directly support queue-like operation, hence using it as a queue is possible but awkward; wrapping ‹std::forward_list› would be, however, a viable approach to this task, too, • ‹std::list› works well as a queue out of the box, but has twice the memory overhead of ‹std::forward_list›. As usual, since we do not yet understand templates, we will only implement a queue of integers, but it is not hard to imagine we could generalize to any type of element. Since we are going for a custom, node-based structure, we will need to first define the class to represent the nodes. For sake of simplicity, we will not encapsulate the attributes. struct queue_node /* C */ { We do not want to handle all the memory management ourselves. To rule out the possibility of accidentally introducing memory leaks, we will use ‹std::unique_ptr› to manage allocated memory for us. Whenever a ‹unique_ptr› is destroyed, it will free up any associated memory. An important limitation of ‹unique_ptr› is that each piece of memory managed by a ‹unique_ptr› must have «exactly one» instance of ‹unique_ptr› pointing to it. When this instance is destroyed, the memory is deallocated. std::unique_ptr< queue_node > next; /* C */ Besides the structure itself, we of course also need to store the actual data. We will store a single integer per node. int value; /* C */ }; We will also need to be able to iterate over the queue. For that, we define an iterator, which is really just a slightly generalized pointer (you may remember ‹nibble_ptr› from last week). We need 3 things: pre-increment, dereference and inequality. struct queue_iterator /* C */ { queue_node *node; The ‹queue› will need to create instances of a ‹queue_iterator›. Let's make that convenient. queue_iterator( queue_node *n ) : node( n ) {} /* C */ The pre-increment operator simply shifts the pointer to the ‹next› pointer of the currently active node. queue_iterator &operator++() /* C */ { node = node->next.get(); return *this; } Inequality is very simple (we need this because the condition of iteration loops is ‹it != c.end()›, including range ‹for› loops): bool operator!=( const queue_iterator &o ) const /* C */ { return o.node != node; } And finally the dereference operator. This should be familiar by now (perhaps notice the ‹const› overload). Depending on element type, the ‹const› overload would in many cases return a ‹const› reference instead of a value. int &operator*() { return node->value; } /* C */ int operator*() const { return node->value; } }; This class represents the queue itself. We will have ‹push› and ‹pop› to add and remove items, ‹empty› to check for emptiness and ‹begin› and ‹end› to implement iteration. class queue /* C */ { We will keep the head of the list in another ‹unique_ptr›. An empty queue will be represented by a null head. Also worth noting is that when using a list as a queue, the head is where we remove items. The end of the queue (where we add new items) is represented by a plain pointer because it does not «own» the node (the node is owned by its predecessor). std::unique_ptr< queue_node > first; /* C */ queue_node *last = nullptr; public: As mentioned above, adding new items is done at the ‘tail’ end of the list. This is quite straightforward: we simply create the node, chain it into the list (using the ‹last› pointer as a shortcut) and point the ‹last› pointer at the newly appended node. We need to handle empty and non-empty lists separately because we chose to represent an empty list using null head, instead of using a dummy node. void push( int v ) /* C */ { if ( last ) /* non-empty list */ { last->next = std::make_unique< queue_node >(); last = last->next.get(); } else /* empty list */ { first = std::make_unique< queue_node >(); last = first.get(); } last->value = v; /* C */ } Reading off the value from the head is easy enough. However, to remove the corresponding node, we need to be able to point ‹first› at the next item in the queue. Unfortunately, we cannot use normal assignment (because copying ‹unique_ptr› is not allowed). We will have to use an operation that is called «move assignment» and which is written using a helper function in from the standard library, called ‹std::move›. Operations which «move» their operands invalidate the «moved-from» instance. In this case, ‹first->next› is the «moved-from» object and the «move» will turn it into a ‹null› pointer. In any case, the ‹next› pointer which was invalidated was stored in the old ‹head› «node» and by rewriting ‹first›, we lost all pointers to that node. This means two things: 1. the old head's ‹next› pointer, now ‹null›, is no longer accessible 2. memory allocated to hold the old head node is freed int pop() /* C */ { int v = first->value; first = std::move( first->next ); Do not forget to update the ‹last› pointer in case we popped the last item. if ( !first ) last = nullptr; /* C */ return v; } The emptiness check is simple enough. bool empty() const { return !last; } /* C */ Now the ‹begin› and ‹end› methods. We start iterating from the head (since we have no choice but to iterate in the direction of the ‹next› pointers). The ‹end› method should return a so-called «past-the-end» iterator, i.e. one that comes right after the last real element in the queue. For an empty queue, both ‹begin› and ‹end› should be the same. Conveniently, the ‹next› pointer in the last real node is ‹nullptr›, so we can use that as our end-of-queue sentinel quite naturally. You may want to go back to the pre-increment operator of ‹queue_iterator› just in case. queue_iterator begin() { return { first.get() }; } /* C */ queue_iterator end() { return { nullptr }; } And finally, erasing elements. Since this is a singly-linked list, to erase an element, we need an iterator to the element «before» the one we are about to erase. This is not really a problem, because erasing at the head is done by ‹pop›. We use the same «move assignment» construct that we have seen in ‹pop› earlier. void erase_after( queue_iterator i ) /* C */ { assert( i.node->next ); i.node->next = std::move( i.node->next->next ); } }; int main() /* demo */ /* C */ { We start by constructing an (empty) queue and doing some basic operations on it. For now, we only try to insert and remove a single element. queue q; /* C */ assert( q.empty() ); q.push( 7 ); assert( !q.empty() ); assert( q.pop() == 7 ); assert( q.empty() ); Now that we have emptied the queue again, we add a few more items and try erasing one and iterating over the rest. q.push( 1 ); /* C */ q.push( 2 ); q.push( 7 ); q.push( 3 ); We check that erase works as expected. We get an iterator that points to the value ‹2› from above and use it to erase the value ‹7›. queue_iterator i = q.begin(); /* C */ ++ i; assert( *i == 2 ); q.erase_after( i ); We can use instances of ‹queue› in range ‹for› loops, because they have ‹begin› and ‹end›, and the types those methods return (i.e. iterators) have dereference, inequality and pre-increment. int x = 1; /* C */ for ( int v : q ) assert( v == x++ ); That went rather well, let's just check that the order of removal is the same as the order of insertion (first in, first out). This is how queues should behave. assert( q.pop() == 1 ); /* C */ assert( q.pop() == 2 ); assert( q.pop() == 3 ); assert( q.empty() ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹finexp›] We will do a simpler version of regular expressions that can only capture finite languages, but somewhat more compactly than just listing all the words that belong to the language. There will be two operations: concatenation and alternative. In this and the next demo, we will make use of late dispatch, which will be properly explained in the next chapter. All you need to know for now is, that, given: • a class ‹base› and its derived class ‹derived›, • a pointer, ‹base *ptr›, that in fact points to an instance of class ‹derived›, and • a method ‹late› which is marked ‹virtual› in ‹base›, and ‹override› in ‹derived›, a call ‹ptr->late()› will execute the implementation of the method from ‹derived› (and not from ‹base›, as would be the case with a non-‹virtual› method). Our goal will be to implement class ‹finexp›, with the following interface: • an instance of ‹finexp› can be constructed from a string; the resulting ‹finexp› will match that exact string and nothing else • two instances of ‹finexp› can be combined using ‹*›: the resulting ‹finexp› matches if the input string can be split in such a way that the first part matches the left ‹finexp› and the second part matches the right ‹finexp› • two instances of ‹finexp› can be comined using ‹+›: the result matches a string if either of the operands does Hint: it might be a worthwhile exercise to compare the below implementation with one based on ‹std::shared_ptr›. struct node; /* C */ using node_ptr = std::unique_ptr< node >; TBD explain things! struct node /* C */ { std::string x; node_ptr l, r; virtual std::set< int > match( const std::string &s ) const /* C */ { assert( !l && !r ); if ( s.substr( 0, x.size() ) == x ) return { int( x.size() ) }; else return {}; } node_ptr copy_into( node_ptr &&n ) const /* C */ { n->l = l ? l->clone() : nullptr; n->r = r ? r->clone() : nullptr; return std::move( n ); } virtual node_ptr clone() const /* C */ { return copy_into( std::make_unique< node >( x ) ); } node( std::string x ) : x( x ) {} /* C */ node( const node_ptr &l_, const node_ptr &r_ ) : l( l_->clone() ), r( r_->clone() ) {} virtual ~node() = default; }; struct alt : node /* C */ { using node::node; node_ptr clone() const override /* C */ { return copy_into( std::make_unique< alt >( x ) ); } std::set< int > match( const std::string &s ) const override /* C */ { std::set< int > lout = l->match( s ), rout = r->match( s ); rout.insert( lout.begin(), lout.end() ); return rout; } }; struct seq : node /* C */ { using node::node; node_ptr clone() const override /* C */ { return copy_into( std::make_unique< seq >( x ) ); } std::set< int > match( const std::string &s ) const override /* C */ { std::set< int > out; for ( int i : l->match( s ) ) /* C */ for ( int j : r->match( s.substr( i ) ) ) out.insert( i + j ); return out; /* C */ } }; class finexp /* C */ { node_ptr n; public: finexp( std::string s ) : n( new node( s ) ) {} finexp( node_ptr &&p ) : n( std::move( p ) ) {} finexp( const finexp &o ) : n( o.n->clone() ) {} finexp operator+( finexp b ) const /* C */ { return { std::make_unique< alt >( n, b.n ) }; } finexp operator*( finexp b ) const /* C */ { return { std::make_unique< seq >( n, b.n ) }; } friend bool match( const finexp &f, const std::string &s ) /* C */ { return f.n->match( s ).count( s.size() ); } }; int main() /* demo */ /* C */ { finexp a( "a" ), b( "b" ), ab( "ab" ), ba( "ba" ), abba( "abba" ); assert( match( a, "a" ) ); /* C */ assert( match( b, "b" ) ); assert( !match( a, "b" ) ); assert( !match( b, "a" ) ); assert( match( abba, "abba" ) ); /* C */ assert( !match( abba, "a" ) ); assert( !match( abba, "abb" ) ); assert( !match( a, "ab" ) ); assert( match( a + b, "a" ) ); /* C */ assert( match( a + b, "b" ) ); assert( !match( a + b, "ab" ) ); assert( !match( a + b, "c" ) ); assert( match( a + abba, "a" ) ); /* C */ assert( !match( a + abba, "b" ) ); assert( match( a + abba, "abba" ) ); assert( match( ( ab + a ) * a, "aba" ) ); /* C */ assert( match( ( a + ab ) * a, "aba" ) ); assert( !match( ( ba + ab ) * a, "ba" ) ); assert( match( a * ( ba + ab ), "aba" ) ); /* C */ assert( !match( a * ( b + a ), "aba" ) ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹expr›] In this example program, we will look at using shared pointers and operator overloading to get a nicer version of our expression examples, this time with sub-structure sharing: that is, doing something like ‹a + a› will not duplicate the sub-expression ‹a›. Like in week 7, we will define an abstract base class to represent the nodes of the expression tree. struct expr_base /* C */ { virtual int eval() const = 0; virtual ~expr_base() = default; }; Since we will use (shared) pointers to ‹expr_base› quite often, we can save ourselves some typing by defining a convenient type alias: ‹expr_ptr› sounds like a reasonable name. using expr_ptr = std::shared_ptr< expr_base >; /* C */ We will have two implementations of ‹expr_base›: one for constant values (nothing much to see here), struct expr_const : expr_base /* C */ { const int value; expr_const( int v ) : value( v ) {} int eval() const override { return value; } }; and another for operator nodes. Those are more interesting, because they need to hold references to the sub-expressions, which are represented as shared pointers. struct expr_op : expr_base /* C */ { enum op_t { add, mul } op; expr_ptr left, right; expr_op( op_t op, expr_ptr l, expr_ptr r ) : op( op ), left( l ), right( r ) {} int eval() const override /* C */ { if ( op == add ) return left->eval() + right->eval(); if ( op == mul ) return left->eval() * right->eval(); assert( false ); } }; In principle, we could directly overload operators on ‹expr_ptr›, but we would like to maintain the illusion that expressions are values. For that reason, we will implement a thin wrapper that provides a more natural interface (and also takes care of operator overloading). Again, the ‹expr› class essentially provides Java-like object semantics -- which is quite reasonable for immutable objects like our expression trees here. struct expr /* C */ { expr_ptr ptr; expr( int v ) : ptr( std::make_shared< expr_const >( v ) ) {} expr( expr_ptr e ) : ptr( e ) {} int eval() const { return ptr->eval(); } }; The overloaded operators simply construct a new node (of type ‹expr_op› and wrap it up in an ‹expr› instance. expr operator+( expr a, expr b ) /* C */ { return { std::make_shared< expr_op >( expr_op::add, a.ptr, b.ptr ) }; } expr operator*( expr a, expr b ) /* C */ { return { std::make_shared< expr_op >( expr_op::mul, a.ptr, b.ptr ) }; } int main() /* demo */ /* C */ { expr a( 3 ), b( 7 ), c( 2 ); expr ab = a + b; expr bc = b * c; expr abc = a + b * c; assert( a.eval() == 3 ); /* C */ assert( b.eval() == 7 ); assert( ab.eval() == 10 ); assert( bc.eval() == 14 ); assert( abc.eval() == 17 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹family›] For many tasks, shared pointers (reference counting) are quite adequate (see also Python). However, they do have a weak spot: reference cycles. If you manage to create a loop of shared pointers, the pointers on this cycle (and anything outside the cycle they point to) will never be freed. That is unfortunate, since it reintroduces memory leaks into the rather leak-free subset of C++ that we have been using until now. However, if we are a little careful, C++ allows us to have cyclic data structures with reference counting without introducing memory leaks: the ‹std::weak_ptr› class template. We will implement a bit of genealogy -- that is, family trees. This will simply consist of a graph of ‹person› instances (we will not delve into too much detail). Each ‹person› will have two parents, a father and a mother, and a list of children. We will want to maintain an invariant: the list of children contains exactly those ‹person› instances that have this ‹person› set as one of their parents. Since a fixed number of pointers (parents) are easier to manage than the arbitrary number of children, we will treat parents as the primary information and children as derived. Like before, we will split the class into a shared (data) part and into thin «interface» part. class person_data /* C */ { std::shared_ptr< person_data > mother, father; std::vector< std::weak_ptr< person_data > > children; std::string name; friend class person; }; The interface: the data is stored behind a shared pointer, but like in earlier examples, we pretend the ‹person› instances are values with sharing semantics. The family graph is, on the outside, still quite immutable (we can only add and remove nodes), so the abstraction is still reasonably solid. class person /* C */ { using data_ptr = std::shared_ptr< person_data >; data_ptr _d; public: Construct a ‹person› instance from an existing data pointer. We would actually like to make this ‹private›, but that would give us problems because we actually delegate the constructor call to ‹std::vector›: we would have to make that a friend class (but that would punch holes into the model... let's not bother for now). explicit person( data_ptr p ) : _d( p ) {} /* C */ We need to be able to construct parent-less instances, since the data ends somewhere and we can no longer provide the data about parents. explicit person( std::string name ) /* C */ : _d( std::make_shared< person_data >() ) { _d->name = name; } The standard constructor for ‹person›, with two parents. We take ‹person› by value since it's really just a pointer anyway (we could perhaps save an refcount increment/decrement pair by passing via ‹const› references). We also use constructor delegation: in the initialization section, we invoke the above ‘parent-less‘ constructor. This constructor is also in charge of maintaining (half of) the above-mentioned invariant by inserting our data pointer into the ‹children› list of both the parents. person( std::string name, person mother, person father ) /* C */ : person( name ) { _d->mother = mother._d; _d->father = father._d; _d->mother->children.emplace_back( _d ); /* C */ _d->father->children.emplace_back( _d ); } The other half of the invariant is maintained here, with the help of ‹shared_ptr› destructors: if a ‹person› is completely destroyed (i.e. no copies remain, i.e. the reference count on the corresponding ‹person_data› drops to zero), all ‹weak_ptr› instances pointing to it will automatically turn into ‹null› pointers. We then simply filter those out to obtain the correct list of children. std::vector< person > children() const /* C */ { std::vector< person > out; for ( const auto &c_weak : _d->children ) if ( auto c = c_weak.lock() ) out.emplace_back( c ); return out; } A few simple accessors. bool valid() const { return !!_d; } /* C */ std::string name() const { return _d->name; } person mother() const { return person{ _d->mother }; } person father() const { return person{ _d->father }; } Equality: we base equality on «object identity»: copies of the same person (even those that arise in a roundabout way, without calling the copy constructor, e.g. those that arise from the above ‹mother› and ‹father› accessors which construct new ‹person› instances) will compare as equal. bool operator==( person o ) const { return o._d == _d; } /* C */ }; int main() /* demo */ /* C */ { person unknown( "unknown" ); person a( "a", unknown, unknown ); person b( "b", unknown, unknown ); assert( a.mother().valid() ); /* C */ assert( a.father().valid() ); assert( a.mother() == unknown ); assert( a.father() == unknown ); assert( !unknown.mother().valid() ); assert( a.mother().name() == "unknown" ); { /* C */ person c( "c", a, b ); person d( "d", a, b ); person x( "x", unknown, unknown ); /* C */ person e( "e", c, x ); Check that the ‹children› containers are correctly filled in by the constructors. assert( c.mother() == a ); /* C */ assert( a.children().size() == 2 ); assert( b.children().size() == 2 ); assert( c.children().size() == 1 ); assert( x.children().size() == 1 ); for ( const auto &ch : x.children() ) /* C */ assert( ch == e ); for ( const auto &ch : c.children() ) /* C */ assert( ch == e ); The instances ‹c›, ‹d›, ‹x› and ‹e› are destroyed at this point (with no surviving copies). } /* C */ Check that the invariant is maintained. assert( a.children().empty() ); /* C */ assert( b.children().empty() ); } ## e. Elementary Exercises ### [‹dynarray›] Implement a dynamic array of integers with 2 operations: element access (using ‹operator[]›) and ‹resize›. The constructor takes the initial size as its only parameter. class dynarray; /* C */ ### [‹list›] Implement a linked list of integers, with ‹head›, ‹tail› (returns a reference) and ‹empty›. Asking for a ‹head› or ‹tail› of an empty list has undefined results. A default-constructed list is empty. The other constructor takes an int (the value of head) and a reference to an existing list. It will should make a copy of the latter. class list; /* C */ ## p. Preparatory Exercises ### [‹unrolled›] Another exercise, another data structure. This time we will look at so-called «unrolled linked lists». We will need the data structure itself, with ‹begin›, ‹end›, ‹empty› and ‹push_back› methods. As usual, we will store integers. The difference between a ‘normal’ singly-linked list and an unrolled list is that in the latter, each node stores more than one item. In this case, we will use 4 items per node. Of course, the last node might only be filled partially. The iterator that ‹begin› and ‹end› return should at least implement dereference, pre-increment and inequality, as usual. We will not provide an interface for erasing elements, because that is somewhat tricky. struct unrolled_node; /* ref: 6 lines */ /* C */ struct unrolled_iterator; /* ref: 22 lines */ class unrolled; /* ref: 36 lines */ ### [‹bittrie›] More data structures. A bit trie (or a bitwise trie, or a bitwise radix tree) is a «binary» tree for encoding a set of binary values, with quick insertion and lookup. Each edge in the tree encodes a single bit (i.e. it carries a zero or a one). To make our life easier, we will represent the keys using a vector of booleans. The key is a sequence of bits: iteration order (left to right) corresponds to a path through the trie starting from the root. I.e. the leftmost bit decides whether to go left or right from the root, and so on. A key is present in the trie iff it describes a path to a leaf node. using key = std::vector< bool >; /* C */ struct trie_node; /* ref: 5 lines */ /* C */ For simplicity, we will not have a normal ‹insert› method. Instead, the trie will expose its root node via ‹root› and allow explicit creation of new nodes via ‹make›, which accepts the parent node and a boolean as arguments (the latter indicating whether the newly created edge represents a 0 or a 1). Both ‹root› and ‹make› should return node references. Finally, add a ‹has› method which will check whether a given key is present in the ‹trie›. class trie; /* ref: 21 lines */ /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹solid›] In this exercise, we will focus on building objects that have optional data attached to them. The idea is that if the optional data is sufficiently big and there are enough instances which do not use this data, it makes sense to split the object into two. Of course, logically (in the interface), the object should still act like a single unit. To make testing easier, we declare a global counter of matrices. It will be adjusted by the constructor and destructor of ‹transform_matrix› below. This is «not» a design pattern that you should normally use (but it is okay in a small demo). int matrix_counter = 0; /* C */ The two pieces will be, in this case, a general description of a 3D object (a solid) and a 3D transformation matrix with 9 entries (3 rows and 3 columns). The matrix is represented by the class declared below. Make the class default-constructible and do not forget to implement the book-keeping for ‹matrix_counter›. The class should store the matrix entries inline (i.e. they should be part of the object, not managed in a separate heap allocation). struct transform_matrix; /* C */ We don't know about inheritance yet, but the below class could be considered a «base class» in a simple «inheritance hierarchy»: it will only have properties common to different object types, but will not describe a complete solid in itself. It should have the following methods: • ‹pos_x›, ‹pos_y› and ‹pos_z› to give the position of the solid • ‹transform_entry( int r, int c )› gives the entry in the transformation matrix at row ‹r› and column ‹c› • ‹transform_set( int r, int c, double v )› sets the corresponding entry in the transformation matrix • a constructor which takes 3 arguments of type ‹double› (the x, y and z position coordinates) The default transformation matrix is the identity matrix (1's on the main diagonal, 0's everywhere else). Memory should only be allocated for the transformation matrix if it changes from the default. class solid; /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹chartrie›] An exercise similar to the ‹bittrie› earlier (same data structure but with bigger keys). To make it more interesting, the node management will happen within the class itself and will not be part of the interface. The encoding you should use is this: • the left child of a node adds a single character to the key, like in the bit trie from before (the character is part of the left edge) • the right child is actually a «sibling» of the current node and the edge is not labelled • the chain to the right is sorted in ascending order In other words, you can imagine the trie to be a 256-ary tree, which is obviously impractical to implement directly (this would need 256 pointers per node). Hence, we encode each ‘virtual’ node in this 256-ary trie using a singly-linked list made of the right children of each real, binary node. struct trie_node; /* ref: 9 lines */ /* C */ The interface of `trie` is very simple: it has an ‹add› method, which inserts a key into the data structure, and a ‹has› method which decides whether a given key is present. Both accept a single ‹std::string›. Like with the bit trie before, we do not consider prefixes of included keys to be present. class trie; /* ref: 53 lines; has() = 10, add() = 36 */ /* C */ ### [‹bdd›] Binary decision diagrams are a compact way to write boolean functions in multiple arguments. You could think of the data structure as a DAG with additional semantics: each vertex is either a «variable» and has two successors which tell us where to go next depending on the value of that variable, or is a 0 or 1, represented by two sink nodes in the DAG (there are no outgoing edges). The interface should be as follows; • the constructor takes a ‹char›: the variable to use for the root node • ‹one› returns the «true» node • ‹zero› returns the «false» node • ‹root› returns the initial node • ‹add_var› takes a char and «creates» a new variable node: there may be multiple nodes for the same variable • ‹add_edge› takes the parent node, a boolean, and the child • ‹eval› takes a map from ‹char› to ‹bool› and returns ‹true› or ‹false› by traversing the BDD from the root and at each variable node, taking the path dictated by the input map (variable assignment) Note: It is UB if a variable node does not have both successors set. class bdd_node; /* ref: 6 lines */ /* C */ class bdd; /* ref: 19 lines */ ### [‹rope›] A rope is a string-like data structure, represented as a binary tree with traditional strings in leaves and weights in internal nodes. Subtree sharing is allowed and expected. A weight of a given node is the total length of the string represented by its left subtree. Provides an O(1) concatenation and O(d) indexing, where d is the depth of the tree. In addition to the indexing operator, provide 2 constructors: one which constructs a singleton rope from a string, and another that joins 2 existing ropes. You do not need to implement any rebalancing. class rope; /* C */ ## r. Regular Exercises ### [‹circular›] In this exercise, we will implement a slightly unusual data structure: a circular linked list, but instead of the usual access operators and iteration, it will have a ‹rotate› method, which rotates the entire list. We require that rotation does not invalidate any references to elements in the list. If you think of the list as a stack, you can think of the ‹rotate› operation as taking an element off the top and putting it at the bottom of the stack. It is undefined on an empty list. To add and remove elements, we will implement ‹push› and ‹pop› which work in a stack-like manner. Only the top element is accessible, via the ‹top› method. This method should allow both read and write access. Finally, we also want to be able to check whether the list is ‹empty›. As always, we will store integers in the data structure. class circular; /* C */ ### [‹zipper›] Implement our favourite data structure – a zipper of integers – this time using a unique_ptr-linked list extending both ways from the focus. Methods: • (constructor) constructs a singleton zipper from an integer, • ‹shift_left› and ‹shift_right› move the point of focus, in O(1), to the nearest left (right) element; they return true if this was possible, otherwise they return false and do nothing, • ‹push_left› and ‹push_right› add a new element just left (just right) of the current focus, again in O(1), • ‹focus› access the current item (read and write). ### [‹segment›] In this exercise, we will go back to building data structures, in this particular case a simple binary tree. The structure should represent a partitioning of an interval with integer bounds into a set of smaller, non-overlapping intervals. Implement class ‹segment_map› with the following interface: • the constructor takes two integers, which represent the limits of the interval to be segmented, • a ‹split› operation takes a single integer, which becomes the start of a new segment, splitting the existing segment in two, • ‹query›, given an integer ‹n›, returns the bounds of the segment that contains ‹n›, as an ‹std::pair› of integers. The tree does «not» need to be self-balancing: the order of splits will determine the shape of the tree. ### [‹diff›] In this exercise, we will implement automatic differentiation of simple expressions. You will need the following rules: • linearity: ⟦ (a⋅f(x) + b⋅g(x))' = a⋅f'(x) + b⋅g'(x) ⟧ • the Leibniz rule: ⟦ (f(x)⋅g(x))' = f'(x)⋅g(x) + f(x)⋅g'(x) ⟧ • chain rule: ⟦ (f(g(x)))' = f'(g(x))⋅g'(x) ⟧ • derivative of exponential: ⟦ exp'(x) = exp(x) ⟧ Define a type, ‹expr› (from «expression»), such that values of this type can be constructed from integers, added and multiplied, and exponentiated using function ‹expnat› (to avoid conflicts with the ‹exp› in the standard library). class expr; /* ref: 29 + 7 lines */ /* C */ expr expnat( expr ); Implement function ‹diff› that accepts a single ‹expr› and returns the derivative (again in the form of ‹expr›). Define a constant ‹x› of type ‹expr› such that ‹diff( x )› is 1. expr diff( expr ); /* ref: 11 lines */ /* C */ // const expr x; Finally, implement function ‹eval› which takes an ‹expr› and a ‹double› and it substitutes for ‹x› and computes the value of the expression. double eval( expr, double ); /* ref: 11 lines */ /* C */ ### [‹critbit›] class cb_tree; bool cb_dir( uint8_t key, uint8_t mask ) /* C */ { return ( 1 + ( mask | key ) ) >> 8; } bool cb_dir( std::string_view key, int byte, uint8_t mask ) /* C */ { return cb_dir( byte < key.size() ? key[ byte ] : 0, mask ); } bool contains( const cb_tree &tree, std::string_view key ) /* C */ { auto n = tree.root(), last = tree.root(); bool last_internal = false; if ( n ) /* C */ while ( n->internal() ) { bool dir = cb_dir( key, n->byte(), n->mask() ); n = dir ? n->right() : n->left(); } return r; /* C */ } # Inheritance and Polymorphism This week will be about objects in the OOP (object-oriented programming) sense and about inheritance-based polymorphism. In OOP, classes are rarely designed in isolation: instead, new classes are «derived» from an existing «base class» (the derived class «inherits from» the base class). The derived class retains all the attributes (data) and methods (behaviours) of the base (parent) class, and usually adds something on top, or at least modifies some of the behaviours. So far, we have worked with «composition» (though we rarely called it that). We say objects (or classes) are composed when attributes of classes are other classes (e.g. standard containers). The relationship between the outer class and its attributes is known as ‘has-a’: a circle «has a» center, a polynomial «has a» sequence of coefficients, etc. Inheritance gives rise to a different type of relationship, known as ‘is-a’: a few stereotypical examples: • a circle «is a» shape, • a ball «is a» solid, a cube «is a» solid too, • a force «is a» vector (and so is velocity). This is where «polymorphism» comes into play: a function which doesn't care about the particulars of a shape or a solid or a vector can accept an instance of the «base class». However, each instance of a derived class «is an» instance of the base class too, and hence can be used in its place. This is known as the Liskov substitution principle. An important caveat: this «does not work» when passing objects «by value», because in general, the base class and the derived class do not have the same size. Languages like Python or Java side-step this issue by always passing objects by reference. In C++, we have to do that explicitly «if» we want to use inheritance-based polymorphism. Of course, this also works with pointers (including smart ones, like ‹std::unique_ptr›). With this bit of theory out of the way, let's look at some practical examples: the rest of theory (late binding in particular) will be explained in demonstrations: 1. ‹account› – a simple inheritance example 2. ‹shapes› – polymorphism and late dispatch 3. ‹expr› – dynamic and static types, more polymorphism 4. ‹destroy› – virtual destructors 5. ‹factory› – polymorphic return values Elementary exercises: 1. ‹resistance› – compute resistance of a simple circuit 2. ‹perimeter› – shapes and their perimeter length 3. ‹fight› – rock, paper and scissors Preparatory exercises: 1. ‹prisoner› – the famous dilemma 2. ‹bexpr› – boolean expressions with variables 3. ‹sexpr› – a tree made of lists (lisp style) 4. ‹network› – a network of counters 5. ‹filter› – filter items from a data source 6. ‹geometry› – shapes and visitors Regular exercises: 1. ‹bom› – polymorphism and collections 2. ‹circuit› – calling virtual methods within the class 3. ‹loops› – circuits with loops 4. ‹pretty› – turn arithmetic expressions into strings 5. ‹json› – a more general JSON pretty-printer 6. ‹while› – interpreting while programs using an AST ## d. Demonstrations ### [‹account›] In this example, we will demonstrate the syntax and most basic use of inheritance. Polymorphism will not enter the picture yet (but we will get to that very soon: in the next example). We will consider bank accounts (a favourite subject, surely). We will start with a simple, vanilla account that has a balance, can withdraw and deposit money. We have seen this before. class account /* C */ { The first new piece of syntax is the ‹protected› keyword. This is related to inheritance: unlike ‹private›, it lets «subclasses» (or rather «subclass methods») access the members declared in a ‹protected› section. We also notice that the balance is signed, even though in this class, that is not strictly necessary: we will need that in one of the subclasses (yes, the system is «already» breaking down a little). protected: /* C */ int _balance; public: /* C */ We allow an account to be constructed with an initial balance. We also allow it to be default-constructed, initializing the balance to 0. account( int initial = 0 ) /* C */ : _balance( initial ) {} Standard stuff. bool withdraw( int sum ) /* C */ { if ( _balance > sum ) { _balance -= sum; return true; } return false; /* C */ } void deposit( int sum ) { _balance += sum; } /* C */ int balance() const { return _balance; } }; With the base class in place, we can define a «derived» class. The syntax for inheritance adds a colon, ‹:›, after the class name and a list of classes to inherit from, with access type qualifiers. We will always use ‹public› inheritance. Also, did you know that naming things is hard? class account_with_overdraft : public account /* C */ { The derived class has, ostensibly, a single attribute. However, all the attributes of all base classes are also present automatically. That is, there already is an ‹int _balance› attribute in this class, inherited from ‹account›. We will use it below. protected: /* C */ int _overdraft; public: /* C */ This is another new piece of syntax that we will need: a constructor of a derived class must first call the constructors of all base classes. Since this happens «before» any attributes of the derived class are constructed, this call comes «first» in the «initialization section». The derived-class constructor is free to choose which (overloaded) constructor of the base class to call. If the call is omitted, the «default constructor» of the base class will be called. account_with_overdraft( int initial = 0, int overdraft = 0 ) /* C */ : account( initial ), _overdraft( overdraft ) {} The methods defined in a base class are automatically available in the derived class as well (same as attributes). However, unlike attributes, we can replace inherited methods with versions more suitable for the derived class. In this case, we need to adjust the behaviour of ‹withdraw›. bool withdraw( int sum ) /* C */ { if ( _balance + _overdraft > sum ) { _balance -= sum; return true; } return false; /* C */ } }; Here is another example based on the same language features. class account_with_interest : public account /* C */ { protected: int _rate; /* percent per annum */ public: /* C */ account_with_interest( int initial = 0, int rate = 0 ) /* C */ : account( initial ), _rate( rate ) {} In this case, all the inherited methods can be used directly. However, we need to «add» a new method, to compute and deposit the interest. Since naming things is hard, we will call it ‹next_year›. The formula is also pretty lame. void next_year() /* C */ { _balance += ( _balance * _rate ) / 100; } }; The way objects are used in this exercise is not super useful: the goal was to demonstrate the syntax and basic properties of inheritance. In modern practice, code re-use through inheritance is frowned upon (except perhaps for mixins, which are however out of scope for this subject). The main use-case for inheritance is «subtype polymorphism», which we will explore in the next unit, ‹shapes.cpp›. int main() /* demo */ /* C */ { We first make a normal account and check that it behaves as expected. Nothing much to see here. account a( 100 ); /* C */ assert( a.balance() == 100 ); assert( a.withdraw( 50 ) ); assert( !a.withdraw( 100 ) ); a.deposit( 10 ); assert( a.balance() == 60 ); Let's try the first derived variant, an account with overdraft. We notice that it's possible to have a negative balance now. account_with_overdraft awo( 100, 100 ); /* C */ assert( awo.balance() == 100 ); assert( awo.withdraw( 50 ) ); assert( awo.withdraw( 100 ) ); awo.deposit( 10 ); assert( awo.balance() == -40 ); And finally, let's try the other account variant, with interest. account_with_interest awi( 100, 20 ); /* C */ assert( awi.balance() == 100 ); assert( awi.withdraw( 50 ) ); assert( !awi.withdraw( 100 ) ); awi.deposit( 10 ); assert( awi.balance() == 60 ); awi.next_year(); assert( awi.balance() == 72 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹shapes›] The inheritance model in C++ is an instance of a more general notion, known as «subtyping». The defining characteristic of subtyping is the Liskov substitution principle: a value which belongs to a «subtype» (a derived class) can be used whenever a variable stores, or a formal argument expects, a value that belongs to a «supertype» (the base class). As mentioned earlier, in C++ this only extends to values passed by «reference» or through pointers. We will first define a couple useful type aliases to represent points and bounding boxes. using point = std::pair< double, double >; /* C */ using bounding_box = std::pair< point, point >; Subtype polymorphism is, in C++, implemented via «late binding»: the decision which method should be called is postponed to runtime (with normal functions and methods, this happens during compile time). The decision whether to use early binding (static dispatch) or late binding (dynamic dispatch) is made by the programmer on a method-by-method basis. In other words, some methods of a class can use static dispatch, while others use dynamic dispatch. class shape /* C */ { public: To instruct the compiler to use dynamic dispatch for a given method, put the keyword ‹virtual› in front of that method's return type. Unlike normal methods, a ‹virtual› method may be left «unimplemented»: this is denoted by the ‹= 0› at the end of the declaration. If a class has a method like this, it is marked as «abstract» and it becomes impossible to create instances of this class: the only way to use it is as a «base class», through inheritance. This is commonly done to define «interfaces». In our case, we will declare two such methods. virtual double area() const = 0; /* C */ virtual bounding_box box() const = 0; A class which introduces ‹virtual› methods also needs to have a «destructor» marked as ‹virtual›. We will discuss this in more detail in a later unit. For now, simply consider this to be an arbitrary rule. virtual ~shape() = default; /* C */ }; As soon as the interface is defined, we can start working with arbitrary classes which implement this interface, even those that have not been defined yet. We will start by writing a simple «polymorphic function» which accepts arbitrary shapes and computes the ratio of their area to the area of their bounding box. double box_coverage( const shape &s ) /* C */ { Hopefully, you remember structured bindings (if not, revisit e.g. ‹03/rel.cpp›). auto [ ll, ur ] = s.box(); /* C */ auto [ left, bottom ] = ll; auto [ right, top ] = ur; return s.area() / ( ( right - left ) * ( top - bottom ) ); /* C */ } Another function: this time, it accepts two instances of ‹shape›. The values it actually receives may be, however, of any type derived from ‹shape›. In fact, ‹a› and ‹b› may be each an instances of a different derived class. bool box_collide( const shape &sh_a, const shape &sh_b ) /* C */ { A helper function (lambda) to decide whether a point is inside (or on the boundary) of a bounding box. auto in_box = []( const bounding_box &box, const point &pt ) /* C */ { auto [ x, y ] = pt; auto [ ll, ur ] = box; auto [ left, bottom ] = ll; auto [ right, top ] = ur; return x >= left && x <= right && y >= bottom && y <= top; /* C */ }; auto [ a, b ] = sh_a.box(); /* C */ auto box = sh_b.box(); The two boxes collide if either of the corners of one is in the other box. return in_box( box, a ) || in_box( box, b ); /* C */ } We now have the interface and two functions that are defined in terms of that interface. To make some use of the functions, however, we need to be able to make instances of ‹shape›, and as we have seen earlier, that is only possible by deriving classes which provide implementations of the virtual methods declared in the base class. Let's start by defining a circle. class circle : public shape /* C */ { point _center; double _radius; public: The base class has a default constructor, so we do not need to explicitly call it here. circle( point c, double r ) : _center( c ), _radius( r ) {} /* C */ Now we need to implement the ‹virtual› methods defined in the base class. In this case, we can omit the ‹virtual› keyword, but we should specify that this method «overrides» one from a base class. This informs the compiler of our «intention» to provide an implementation to an inherited method and allows it (the compiler) to emit a warning in case we accidentally «hide» the method instead, by mistyping the signature. The most common mistake is forgetting the trailing ‹const›. Please always specify ‹override› where it is applicable. double area() const override /* C */ { return 4 * std::atan( 1 ) * std::pow( _radius, 2 ); } Now the other ‹virtual› method. bounding_box box() const override /* C */ { auto [ x, y ] = _center; double r = _radius; return { { x - r, y - r }, { x + r, y + r } }; } }; And a second shape type, so we can actually make some use of polymorphism. Everything is the same as above. class rectangle : public shape /* C */ { point _ll, _ur; /* lower left, upper right */ public: rectangle( point ll, point ur ) : _ll( ll ), _ur( ur ) {} /* C */ double area() const override /* C */ { auto [ left, bottom ] = _ll; auto [ right, top ] = _ur; return ( right - left ) * ( top - bottom ); } bounding_box box() const override /* C */ { return { _ll, _ur }; } }; int main() /* demo */ /* C */ { We cannot directly construct a ‹shape›, since it is «abstract», i.e. it has unimplemented «pure virtual methods». However, both ‹circle› and ‹rectangle› provide implementations of those methods which we can use. rectangle square( { 0, 0 }, { 1, 1 } ); /* C */ assert( square.area() == 1 ); assert( square.box() == bounding_box( { 0, 0 }, { 1, 1 } ) ); assert( box_coverage( square ) == 1 ); circle circ( { 0, 0 }, 1 ); /* C */ Check that the area of a unit circle is π, and the ratio of its area to its bounding box is π / 4. double pi = 4 * std::atan( 1 ); /* C */ assert( std::fabs( circ.area() - pi ) < 1e-10 ); assert( std::fabs( box_coverage( circ ) - pi / 4 ) < 1e-10 ); The two shapes quite clearly collide, and if they collide, their bounding boxes must also collide. A shape should always collide with itself, and collisions are symmetric, so let's check that too. assert( box_collide( square, circ ) ); /* C */ assert( box_collide( circ, square ) ); assert( box_collide( square, square ) ); assert( box_collide( circ, circ ) ); Let's make a shape a bit further out and check the collision detection with that. circle c1( { 2, 3 }, 1 ), c2( { -1, -1 }, 1 ); /* C */ assert( !box_collide( circ, c1 ) ); assert( !box_collide( c1, c2 ) ); assert( !box_collide( c1, square ) ); assert( box_collide( c2, square ) ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹expr›] To better understand polymorphism, we will need to set up some terminology, particularly: • the notion of a «static type», which is, essentially, the type written down in the source code, and of a • «dynamic type» (also known as a «runtime type»), which is the actual type of the value that is stored behind a given reference (or pointer). The relationship between the «static» and «dynamic» type may be: • the static and dynamic type are the same (this was always the case until this week), or • the dynamic type may be a «subtype» of the static type (we will see that in a short while). Anything else is a bug. We will use a very simple representation of arithmetic expressions as our example here. An expression is a «tree», where each «node» carries either a «value» or an «operation». We will want to explicitly track the type of each node, and for that, we will use an «enumerated type». Those work the same as in C, but if we declare them using ‹enum class›, the enumerated names will be «scoped»: we use them as ‹type::sum›, instead of just ‹sum› as would be the case in C. enum class type { sum, product, constant }; /* C */ Now for the class hierarchy. The base class will be ‹node›. class node /* C */ { public: The first thing we will implement is a ‹static_type› method, which tells us the static type of this class. The base class, however, does not have any sensible value to return here, so we will just throw an exception. type static_type() const /* C */ { throw std::logic_error( "bad static_type() call" ); } The ‘real’ (dynamic) type must be a ‹virtual› method, since the actual implementation must be selected based on the «dynamic type»: this is exactly what late binding does. Since the method is ‹virtual›, we do not need to supply an implementation if we can't give a sensible one. virtual type dynamic_type() const = 0; /* C */ The interesting thing that is associated with each node is its «value». For operation nodes, it can be computed, while for leaf nodes (type ‹constant›), it is simply stored in the node. virtual int value() const = 0; /* C */ We also observe the «virtual destructor rule». virtual ~node() = default; /* C */ }; We first define the (simpler) leaf nodes, i.e. constants. class constant : public node /* C */ { int _value; public: The leaf node constructor simply takes an integer value and stores it in an attribute. constant( int v ) : _value( v ) {} /* C */ Now the interface common to all ‹node› instances: type static_type() const { return type::constant; } /* C */ In methods of class ‹constant›, the «static type» of ‹this› is always¹ either ‹constant *› or ‹const constant *›. Hence we can simply call the ‹static_type› method, since it uses «static dispatch» (it was not declared ‹virtual› in the base class) and hence the call will always resolve to the method just above. type dynamic_type() const override { return static_type(); } /* C */ Finally, the ‘business’ method: int value() const override { return _value; } /* C */ }; ¹ As long as we pretend that the ‹volatile› keyword does not exist, which is an entirely reasonable thing to do. The inner nodes of the tree are «operations». We will create an intermediate (but still abstract) class, to serve as a base for the two operation classes which we will define later. class operation : public node /* C */ { const node &_left, &_right; public: /* C */ operation( const node &l, const node &r ) : _left( l ), _right( r ) {} We will leave ‹static_type› untouched: the version from the base class works okay for us, since there is nothing better that we could do here. The ‹dynamic_type› and ‹value› stay unimplemented. We are facing a dilemma here, though. We would like to add accessors for the children, but it is not clear whether to make them ‹virtual› or not. Considering that we keep the references in attributes of this class, it seems unlikely that the implementation of the accessors would change in a subclass and we can use cheaper «static dispatch». const node &left() const { return _left; } /* C */ const node &right() const { return _right; } }; Now for the two operation classes. class sum : public operation /* C */ { public: The base class does not have a default constructor, which means we need to call the one that's available manually. sum( const node &l, const node &r ) /* C */ : operation( l, r ) {} We want to replace the ‹static_type› implementation that was inherited from ‹node› (through ‹operation›): type static_type() const { return type::sum; } /* C */ And now the (dynamic-dispatch) interface mandated by the (indirect) base class ‹node›. We can use the same approach that we used in ‹constant› for ‹dynamic_type›: type dynamic_type() const override { return static_type(); } /* C */ And finally the logic. The «static return type» of ‹left› and ‹right› is ‹const node &›, but the method we call on each, ‹value›, uses dynamic dispatch (it is marked ‹virtual› in class ‹node›). Therefore, the actual method which will be called depends on the «dynamic type» of the respective child node. int value() const override /* C */ { return left().value() + right().value(); } }; Basically a re-run of ‹sum›. class product : public operation /* C */ { public: We will use a trick which will allow us to not type out the (boring and redundant) constructor. If all we want to do is just forward arguments to the parent class, we can use the following syntax. You do not have to remember it, but it can save some typing if you do. using operation::operation; /* C */ Now the interface methods. type static_type() const { return type::product; } /* C */ type dynamic_type() const override { return static_type(); } int value() const override /* C */ { return left().value() * right().value(); } }; int main() /* demo */ /* C */ { Instances of class ‹constant› are quite straightforward. Let's declare some. constant const_1( 1 ), /* C */ const_2( 2 ), const_m1( -1 ), const_10( 10 ); The constructor of ‹sum› accepts two instances of ‹node›, passed by reference. Since ‹constant› is a subclass of ‹node›, it is okay to use those, too. sum sum_0( const_1, const_m1 ), /* C */ sum_3( const_1, const_2 ); The ‹product› constructor is the same. But now we will also try using instances of ‹sum›, since ‹sum› is also derived (even if indirectly) from ‹node› and therefore ‹sum› is a subtype of ‹node›, too. product prod_4( const_2, const_2 ), /* C */ prod_6( const_2, sum_3 ), prod_40( prod_4, const_10 ); Let's also make a ‹sum› instance which has children of different types. sum sum_9( sum_3, prod_6 ); /* C */ For all variables which hold «values» (i.e. not references), «static type» = «dynamic type». To make the following code easier to follow, the static type of each of the above variables is explicitly mentioned in its name. Clearly, we can call the ‹value› method on the variables directly and it will call the right method. assert( const_1.value() == 1 ); /* C */ assert( const_2.value() == 2 ); assert( sum_0.value() == 0 ); assert( sum_3.value() == 3 ); assert( prod_4.value() == 4 ); assert( prod_6.value() == 6 ); assert( prod_40.value() == 40 ); assert( sum_9.value() == 9 ); However, the above results should already convince us that dynamic dispatch works as expected: the results depend on the ability of ‹sum::value› and ‹product::value› to call correct versions of the ‹value› method on their children, even though the «static types» of the references stored in ‹operation› are ‹const node›. We can however explore the behaviour in a bit more detail. const node &sum_0_ref = sum_0, &prod_6_ref = prod_6; /* C */ Now the «static type» of ‹sum_0_ref› is ‹const node &›, but the «dynamic type» of the value to which it refers is ‹sum›, and for ‹prod_6_ref› the static type is ‹const node &› and dynamic is ‹product›. assert( sum_0_ref.value() == 0 ); /* C */ assert( prod_6_ref.value() == 6 ); Let us also check the behaviour of ‹left› and ‹right›. assert( sum_0.left().value() == 1 ); /* C */ assert( sum_0.right().value() == -1 ); The «static type» through which we call ‹left› and ‹right› does not matter, because neither ‹product› nor ‹sum› provide a different implementation of the method. const operation &op = sum_0; /* C */ assert( op.left().value() == 1 ); assert( op.right().value() == -1 ); The final thing to check is the ‹static_type› and ‹dynamic_type› methods. By now, we should have a decent understanding of what to expect. Please note that ‹sum_0› and ‹sum_0_ref› refer to the «same instance» and hence they have the same «dynamic type», even though their «static types» differ. assert( sum_0.dynamic_type() == type::sum ); /* C */ assert( sum_0_ref.dynamic_type() == type::sum ); assert( sum_0.static_type() == type::sum ); /* C */ try { sum_0_ref.static_type(); assert( false ); } /* C */ catch ( const std::logic_error & ) {} And the same is true about ‹prod_6› and ‹prod_6_ref›. assert( prod_6.dynamic_type() == type::product ); /* C */ assert( prod_6_ref.dynamic_type() == type::product ); assert( prod_6.static_type() == type::product ); try { prod_6_ref.static_type(); assert( false ); } /* C */ catch ( const std::logic_error & ) {} } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹destroy›] In this (entirely synthetic, sorry) example, we will look at object destruction, especially in the context of polymorphism. We first set up a few counters to track constructor and destructor calls. static int bad_base_counter = 0, bad_derived_counter = 0, /* C */ good_base_counter = 0, good_derived_counter = 0; class bad_base /* C */ { public: virtual int bad_dummy() { return 0; } bad_base() { bad_base_counter ++; } /* C */ We will knowingly break the «virtual destructor rule» here, to see «why» the rule exists. ~bad_base() { bad_base_counter --; } /* C */ }; class good_base /* C */ { public: virtual int good_dummy() { return 0; } good_base() { good_base_counter ++; } /* C */ Notice the ‹virtual›. virtual ~good_base() { good_base_counter --; } /* C */ }; Let's add some innocent derived classes. class bad_derived : public bad_base /* C */ { public: bad_derived() { bad_derived_counter ++; } ~bad_derived() { bad_derived_counter --; } }; class good_derived : public good_base /* C */ { public: good_derived() { good_derived_counter ++; } It is good practice to also add ‹override› to destructors of derived classes. This will tell the compiler we expect the base class to have a ‹virtual› destructor which we are extending. The compiler will emit an error if the base class destructor is (through some unfortunate accident) not marked as ‹virtual›. ~good_derived() override { good_derived_counter --; } /* C */ }; int main() /* demo */ /* C */ { For regular variables, everything works as expected: constructors and destructors of all classes in the hierarchy are called. { /* C */ bad_base bb; assert( bad_base_counter == 1 ); bad_derived bd; assert( bad_base_counter == 2 ); assert( bad_derived_counter == 1 ); } assert( bad_base_counter == 0 ); /* C */ assert( bad_derived_counter == 0 ); Same thing with virtual destructors. { /* C */ good_base gb; assert( good_base_counter == 1 ); good_derived gd; assert( good_base_counter == 2 ); assert( good_derived_counter == 1 ); } assert( good_base_counter == 0 ); /* C */ assert( good_derived_counter == 0 ); However, problems start if an instance is destroyed through a pointer whose static type disagrees with the dynamic type. This cannot happen with references (unless the destructor is called explicitly), but it is entirely plausible with pointers, including smart pointers. Let's first demonstrate the case that works: ‹good_derived›. using good_ptr = std::unique_ptr< good_base >; /* C */ Please make good note of the fact, that the static type of the pointer refers to ‹good_base›, but the actual value stored in it has dynamic type ‹good_derived›. { /* C */ good_ptr gp = std::make_unique< good_derived >(); assert( good_base_counter == 1 ); assert( good_derived_counter == 1 ); } Since the ‹unique_ptr› went out of scope, the instance stored behind it was destroyed. The counters should be both zero again. assert( good_base_counter == 0 ); /* C */ assert( good_derived_counter == 0 ); Let's observe what happens with the ‹bad_base› and ‹bad_derived› combination. using bad_ptr = std::unique_ptr< bad_base >; /* C */ { /* C */ bad_ptr bp = std::make_unique< bad_derived >(); assert( bad_base_counter == 1 ); assert( bad_derived_counter == 1 ); } The pointer went out of scope. Since the destructor was called using «static dispatch», only the «base class» destructor was called. This is of course very problematic, since resources were leaked and invariants broken. assert( bad_base_counter == 0 ); /* C */ assert( bad_derived_counter == 1 ); Please note that some compilers (recent ‹clang› versions) will «emit a warning» if this happens. Unfortunately, this is not the case with ‹gcc› 9.2 which we are using (and which is a rather recent compiler). It is therefore unadvisable to rely on the compiler to catch this type of problem. Stay vigilant. } /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹factory›] As we have seen, subtype polymorphism allows us to define an «interface» in terms of ‹virtual› methods (that is, based on late dispatch) and then create various «implementations» of this interface. It is sometimes useful to create instances of multiple different derived classes based on runtime inputs, but once they are created, to treat them uniformly. The uniform treatment is made possible by subtype polymorphism: if the entire interaction with these objects is done through the shared interface, the instances are all, at the type level, interchangeable with each other. The behaviour of those instances will of course differ, depending on their «dynamic type». When a system is designed this way, the entire program uses a single «static type» to work with all instances from the given inheritance hierarchy -- the type of the base class. Let's define such a base class. class part /* C */ { public: virtual std::string description() const = 0; virtual ~part() = default; }; Let's add a simple function which operates on generic parts. Working with instances is easy, since they can be passed through a reference to the base type. For instance the following function which formats a single line for a bill of materials (bom). std::string bom_line( const part &p, int count ) /* C */ { return std::to_string( count ) + "x " + p.description(); } However, «creation» of these instances poses a somewhat unique challenge in C++: memory management. In languages like Java or C#, we can create the instance and return a reference to the caller, and the garbage collector will ensure that the instance is correctly destroyed when it is no longer used. We do not have this luxury in C++. Of course, we could always do memory management by hand, like it's 1990. Fortunately, modern C++ provides «smart pointers» in the standard library, making memory management much easier and safer. Recall that a ‹unique_ptr› is an «owning» pointer: it holds onto an object instance while it is in scope and destroys it afterwards. Unlike objects stored in local variables, though, the ownership of the instance held in a ‹unique_ptr› can be transferred out of the function (i.e. an instance of ‹unique_ptr› can be legally returned, unlike a reference to a local variable). This will make it possible to define a «factory»: a function which constructs instances (parts) and returns them to the caller. Of course, to actually define the function, we will need to define the derived classes which it is supposed to create. using part_ptr = std::unique_ptr< part >; /* C */ part_ptr factory( std::string ); In the program design outlined earlier, the derived classes change some of the behaviours, or perhaps add data members (attributes) to the base class, but apart from construction, they are entirely operated through the interface defined by the base class. class cog : public part /* C */ { int teeth; public: cog( int teeth ) : teeth( teeth ) {} std::string description() const override /* C */ { return std::string( "cog with " ) + std::to_string( teeth ) + " teeth"; } }; class axle : public part /* C */ { public: std::string description() const override { return "axle"; } }; class screw : public part /* C */ { int _thread, _length; public: screw( int t, int l ) : _thread( t ), _length( l ) {} /* C */ std::string description() const override /* C */ { return std::to_string( _length ) + "mm M" + std::to_string( _thread ) + " screw"; } }; Now that we have defined the derived classes, we can finally define the factory function. part_ptr factory( std::string desc ) /* C */ { We will use ‹std::istringstream› (first described in ‹06/streams.cpp›) to extract a description of the instance that we want to create from a string. The format will be simple: the type of the part, followed by its parameters separated by spaces. std::istringstream s( desc ); /* C */ std::string type; s >> type; /* extract the first word */ if ( type == "cog" ) /* C */ { int teeth; s >> teeth; return std::make_unique< cog >( teeth ); } if ( type == "axle" ) /* C */ return std::make_unique< axle >(); if ( type == "screw" ) /* C */ { int thread, length; s >> thread >> length; return std::make_unique< screw >( thread, length ); } throw std::runtime_error( "unexpected part description" ); /* C */ } int main() /* demo */ /* C */ { Let's first use the factory to make some instances. They will be held by ‹part_ptr› (i.e. ‹unique_ptr› with the static type ‹part›. part_ptr ax = factory( "axle" ), /* C */ m7 = factory( "screw 7 50" ), m3 = factory( "screw 3 10" ), c8 = factory( "cog 8" ), c9 = factory( "cog 9" ); From the point of view of the static type system, all the parts created above are now the same. We can call the methods which were defined in the interface, or we can pass them into functions which work with parts. assert( ax->description() == "axle" ); /* C */ assert( m7->description() == "50mm M7 screw" ); assert( m3->description() == "10mm M3 screw" ); assert( c8->description() == "cog with 8 teeth" ); assert( c9->description() == "cog with 9 teeth" ); Let's try using the ‹bom_line› function which we have defined earlier. assert( bom_line( *ax, 3 ) == "3x axle" ); /* C */ assert( bom_line( *m7, 20 ) == "20x 50mm M7 screw" ); At the end of the scope, the objects are destroyed and all memory is automatically freed. } /* C */ ## e. Elementary Exercises ### [‹resistance›] We are given a simple electrical circuit made of resistors and wires, and we want to compute the total resistance between two points. The circuit is simple in the sense that in any given section, all its immediate sub-sections are either connected in series or in parallel. Here is an example: ┌────┐ ┌─────╴│ R₂ │╶─────┐ ┌────┐ │ └────┘ │ ┌────┐ ●╶╴│ R₁ │╶╴● ●╶╴│ R₅ │╶╴● A └────┘ │ ┌────┐ ┌────┐ │ └────┘ B └─╴│ R₃ │╶╴│ R₄ │╶─┘ └────┘ └────┘ The resistance that we are interested in is between the points A and B. Given ⟦R₁⟧ and ⟦R₂⟧ connected in series, the total resistance is ⟦R = R₁ + R₂⟧. For the same resistors connected in parallel, the resistance is given by ⟦1/R = 1/R₁ + 1/R₂⟧. You will implement 2 classes: ‹series› and ‹parallel›, each of which represents a single segment of the circuit. Both classes shall provide a method ‹add›, that will accept either a number (‹double›) which will add a single resistor to that segment, or a ‹const› reference to the opposite class (i.e. an instance of ‹series› should accept a reference to ‹parallel› and vice versa). class series; /* C */ class parallel; Then add a top-level function ‹resistance›, which accepts either a ‹series› or a ‹parallel› instance and computes the total resistance of the circuit described by that instance. The exact prototype is up to you. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹perimeter›] Implement a simple inheritance hierarchy – the base class will be ‹shape›, with a pure virtual method ‹perimeter›, the 2 derived classes will be ‹circle› and ‹rectangle›. The circle is constructed from a radius, while the rectangle from a width and height, all of them floating-point numbers. class shape; /* C */ class circle; class rectangle; bool check_shape( const shape &s, double p ) /* C */ { return std::fabs( s.perimeter() - p ) < 1e-8; } ### [‹fight›] There should be 4 classes: the base class ‹gesture› and 3 derived: ‹rock›, ‹paper› and ‹scissors›. Class ‹gesture› has a (pure virtual) method ‹fight› which takes another gesture (via a const reference) and returns ‹true› if the current gesture wins. To do this, add another method, ‹visit›, which has 3 overloads, one each for ‹rock›, ‹paper› and ‹scissors›. Then override ‹fight› in each derived class, to simply call ‹visit( *this )› on the opposing gesture. The ‹visit› method knows the type of both ‹this› and the opponent (via the overload) – simply indicate the winner by returning an appropriate constant. class rock; /* C */ class paper; class scissors; Keep the forward declarations, you will need them to define the overloads for ‹visit›. class gesture; /* C */ Now define the 3 derived classes. ## p. Preparatory Exercises ### [‹prisoner›] Another exercise, another class hierarchy. The «abstract base class» will be called ‹prisoner›, and the implementations will be different strategies in the well-known game of (iterated) prisoner's dilemma. The ‹prisoner› class should provide method ‹betray› which takes a boolean (the decision of the other player in the last round) and returns the decision of the player for this round. In general, the ‹betray› method should «not» be ‹const›, because strategies may want to remember past decisions (though we will not implement a strategy like that in this exercise). class prisoner; /* C */ Implement an always-betray strategy in class ‹traitor›, the tit-for-tat strategy in ‹vengeful› and an always-cooperate in ‹benign›. class traitor; /* C */ class vengeful; class benign; Implement a simple strategy evaluator in function ‹play›. It takes two prisoners and the number of rounds and returns a negative number if the first one wins, 0 if the game is a tie and a positive number if the second wins. The scoring matrix: • neither player betrays 2 / 2 • a betrays, b does not: 3 / 0 • a does not betray, b does: 0 / 3 • both betray 1 / 1 int play( prisoner &a, prisoner &b, int rounds ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹bexpr›] Boolean expressions with variables, represented as binary trees. Internal nodes carry a logical operation on the values obtained from children while leaf nodes carry variable references. To evaluate an expression, we will need to supply values for each of the variables that appears in the expression. We will identify variables using integers, and the assignment of values will be done through the type ‹input› defined below. It is undefined behaviour if a variable appears in an expression but is not present in the provided ‹input› value. using input = std::map< int, bool >; /* C */ Like earlier in ‹expr.cpp›, the base class will be called ‹node›, but this time will only define a single method: ‹eval›, which accepts a single ‹input› argument (as a ‹const› reference). class node; /* ref: 6 lines */ /* C */ Internal nodes are all of the same type, and their constructor takes an unsigned integer, ‹table›, and two ‹node› references. Assuming bit zero is the lowest-order bit, the node operates as follows: • ‹false› ‹false› → bit 0 of ‹table› • ‹false› ‹true› → bit 1 of ‹table› • ‹true› ‹false› → bit 2 of ‹table› • ‹true› ‹true› → bit 3 of ‹table› class operation; /* ref: 16 lines */ /* C */ The leaf nodes carry a single integer (passed in through the constructor) -- the identifier of the variable they represent. class variable; /* ref: 7 lines */ /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹sexpr›] An s-expression is a tree in which each node has an arbitrary number of children. To make things a little more interesting, our s-expression nodes will own their children. The base class will be called ‹node› (again) and it will have single (virtual) method: ‹value›, with no arguments and an ‹int› return value. class node; /* C */ using node_ptr = std::unique_ptr< node >; There will be two types of internal nodes: ‹sum› and ‹product›, and in this case, they will compute the sum or the product of all their children, regardless of their number. A sum with no children should evaluate to 0 and a product with no children should evaluate to 1. Both will have an additional method: ‹add_child›, which accepts (by value) a single ‹node_ptr› and both should have default constructors. It is okay to add an intermediate class to the hierarchy. class sum; /* C */ class product; Leaf nodes carry an integer constant, given to them via a constructor. class constant; /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹network›] In this exercise, we will define a network of counters, where each node has its own counter which starts at zero, and events which affect the counters propagate in the network. Different node types react differently to the events. There are three basic events which can propagate through the network: ‹reset› will set the counter to 0, ‹increment› and ‹decrement› add and subtract 1, respectively. enum class event { reset, increment, decrement }; /* C */ The «abstract base class», ‹node›, will define the polymorphic interface. Methods: • ‹react› with a single argument of type ‹event›, • ‹connect› which will take a reference to another ‹node› instance: the connection thus created starts in ‹this› and extends to the ‹node› given in the argument, • ‹read›, a ‹const› method that returns the current value of the counter. Think carefully about which methods need to be ‹virtual› and which don't. The counter is signed and starts at 0. Each node can have an arbitrary number of both outgoing and incoming connections. class node; /* C */ Now for the node types. Each node type first applies the event to its own counter, then propagates (or not) some event along all outgoing connections. Implement the following node types: • ‹forward› sends the same event it has received • ‹invert› sends the opposite event • ‹gate› resends the event if the new counter value is positive class forward; /* C */ class invert; class gate; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹filter›] This exercise will be yet another take on a set of numbers. This time, we will add a capability to filter the numbers on output. It will be possible to change the filter applied to a given set at runtime. The «base class» for representing filters will contain a single pure ‹virtual› method, ‹accept›. The method should be marked ‹const›. class filter; /* C */ The ‹set› (which we will implement below) will «own» the filter instance and hence will use a ‹unique_ptr› to hold it. using filter_ptr = std::unique_ptr< filter >; /* C */ The ‹set› should have standard methods: ‹add› and ‹has›, the latter of which will respect the configured filter (i.e. items rejected by the filter will always test negative on ‹has›). The method ‹set_filter› should set the filter. If no filter is set, all numbers should be accepted. Calling ‹set_filter› with a ‹nullptr› argument should clear the filter. Additionally, ‹set› should have ‹begin› and ‹end› methods (both ‹const›) which return very simple iterators that only provide «dereference» to an ‹int› (value), pre-increment and inequality. It is a good idea to keep «two» instances of ‹std::set< int >::iterator› in attributes (in addition to a pointer to the output filter): you will need to know, in the pre-increment operator, that you ran out of items when skipping numbers which the filter rejected. class set_iterator; /* C */ class set; Finally, implement a filter that only accepts odd numbers. class odd; /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹geometry›] We will go back to a bit of geometry, this time with circles and lines: in this exercise, we will be interested in planar intersections. We will consider two objects to intersect when they have at least one common point. On the C++ side, we will use a bit of a trick with ‹virtual› method overloading (in a slightly more general setting, the trick is known as the «visitor pattern»). First some definitions: the familiar ‹point›. using point = std::pair< double, double >; /* C */ Check whether two floating-point numbers are ‘essentially the same’ (i.e. fuzzy equality). bool close( double a, double b ) /* C */ { return std::fabs( a - b ) < 1e-10; } We will need to use forward declarations in this exercise, since methods of the base class will refer to the derived types. struct circle; /* C */ struct line; These two helper functions are already defined in this file and may come in useful (like the ‹slope› class above). double dist( point, point ); /* C */ double dist( const line &, point ); A helper class which is constructed from two points. Two instances of ‹slope› compare equal if the slopes of the two lines passing through the respective point pairs are the same. struct slope : std::pair< double, double > /* C */ { slope( point p, point q ) : point( ( q.first - p.first ) / dist( p, q ), ( q.second - p.second ) / dist( p, q ) ) {} bool operator==( const slope &o ) const /* C */ { auto [ px, py ] = *this; auto [ qx, qy ] = o; return ( close( px, qx ) && close( py, qy ) ) || /* C */ ( close( px, -qx ) && close( py, -qy ) ); } bool operator!=( const slope &o ) const /* C */ { return !( *this == o ); } }; Now we can define the class ‹object›, which will have a ‹virtual› method ‹intersects› with 3 overloads: one that accepts a ‹const› reference to a ‹circle›, another that accepts a ‹const› reference to a ‹line› and finally one that accepts any ‹object›. class object; /* C */ Put definitions of the classes ‹circle› and ‹line› here. A ‹circle› is given by a ‹point› and a radius (‹double›), while a ‹line› is given by two points. NB. Make the ‹line› attributes ‹public› and name them ‹p› and ‹q› to make the ‹dist› helper function work. struct circle; /* ref: 18 lines */ /* C */ struct line; /* ref: 18 lines */ Definitions of the helper functions. double dist( point p, point q ) /* C */ { auto [ px, py ] = p; auto [ qx, qy ] = q; return std::sqrt( std::pow( px - qx, 2 ) + std::pow( py - qy, 2 ) ); } double dist( const line &l, point p ) /* C */ { auto [ x2, y2 ] = l.q; auto [ x1, y1 ] = l.p; auto [ x0, y0 ] = p; return std::fabs( ( y2 - y1 ) * x0 - ( x2 - x1 ) * y0 + /* C */ x2 * y1 - y2 * x1 ) / dist( l.p, l.q ); } ## r. Regular Exercises ### [‹bom›] Let's revisit the idea of a bill of materials that made a brief appearance in ‹factory.cpp›, but in a slightly more useful incarnation. Define the following class hierarchy: the base class, ‹part›, should have a (pure) virtual method ‹description› that returns an ‹std::string›. It should also keep an attribute of type ‹std::string› and provide a getter for this attribute called ‹part_no()› (part number). Then add 2 derived classes: • ‹resistor› which takes the part number and an integral resistance as its constructor argument and provides a description of the form ‹"resistor ?Ω"› where ‹?› is the provided resistance, • ‹capacitor› which also takes a part number and an integral capacitance and provides a description of the form ‹"capacitor ?μF"› where ‹?› is again the provided value. class part; /* C */ class resistor; class capacitor; We will also use owning pointers, so let us define a convenient type alias for that: using part_ptr = std::unique_ptr< part >; /* C */ That was the mechanical part. Now we will need to think a bit: we want a class ‹bom› which will remember a list of parts, along with their quantities and will «own» the ‹part› instances it holds. The interface: • a method ‹add›, which accepts a ‹part_ptr› «by value» (it will take ownership of the instance) and the quantity (integer) • a method ‹find› which accepts an ‹std::string› and returns a ‹const› reference to the part instance with the given part number, • a method ‹qty› which returns the associated quantity, given a part number. class bom; /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹circuit›] In this exercise, we will look at calling ‹virtual› methods from within the class, in an ‘inverted’ approach to inheritance. Most of the implementation will be part of the «base class», in terms of a few (or in this case one) ‹protected virtual› methods. We will implement a simple class hierarchy to represent a «logical circuit»: a bunch of components connected with wires. Each component will have at most 2 inputs and a single output (all of which are boolean values). Implement the following (non-virtual) methods: • ‹connect› which takes an integer (0 or 1, the index of the input to connect) and a reference to another ‹component› and connects the output of the given component to the input of this component • ‹read› with no arguments, which returns the current output of the component (this will of course depend on the state of the input components). Both inputs start out unconnected. Unconnected inputs always read out ‹false›. Behaviour is undefined if there is a loop in the circuit (but see also ‹loops.cpp›). class component; /* C */ The derived classes should be as follows: • ‹nand› for which the output is the NAND logical function of the two inputs, • ‹source› which ignores both inputs and reads out ‹true›, • ‹delay› which behaves as follows: first time ‹read› is called, it always returns zero; subsequent ‹read› calls return a value that the input 0 had at the time of the previous call to ‹read›. class nand; /* C */ class source; class delay; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹loops›] Same basic idea as ‹circuit.cpp›: we model a circuit made of components. Things get a bit more complicated in this version: • loops are allowed • parts have 2 inputs and 2 outputs each The base class, with the following interface: • ‹read› takes an integer (decides which output) and returns a boolean, • ‹connect› takes two integers and a reference to a component (the first integer defines the input of ‹this› and the second integer defines the output of the third argument to connect). There is more than one way to resolve loops, some of which require ‹read› to be virtual (that's okay). Please note that each loop «must» have at least one ‹delay› in it (otherwise, behaviour is still undefined). NB. Each component should first read input 0 and then input 1: the ordering will affect the result. class component; /* ref: 30 lines */ /* C */ A ‹delay› is a component that reads out, on both outputs, the value it has obtained on the corresponding input on the previous call to ‹read›. class delay; /* ref: 20 lines */ /* C */ A ‹latch› remembers one bit of information (starting at ‹false›): • if both inputs read ‹false›, the remembered bit remains unchanged, • if input 0 is ‹false› while input 1 is ‹true› the remembered bit is set to ‹true›, • in all other cases, the remembered bit is set to ‹false›. The value on output 0 is the «new value» of the remembered bit: there is no delay. The value on output 1 is the negation of output 0. class latch; /* 15 lines */ /* C */ Finally, the ‹cnot› gate, or a «controlled not» gate has the following behaviour: • output 0 always matches input 0, while • output 1 is set to: ◦ input 1 if input 0 is ‹true› ◦ negation of input 1 if input 0 is ‹false› class cnot; /* ref: 11 lines */ /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹pretty›] In this exercise, we will write a pretty-printer for simple arithmetic expressions, with 3 operation types: addition, multiplication and equality, written as ‹+›, ‹*› and ‹=› respectively. The goal is to print the expression with as few parentheses as possible. Assume full associativity for all operations. The precedence order is the usual one: multiplication binds most tightly, while equality most loosely. The formatting is done by calling a ‹print› method on the root of the expression to be printed. class node; /* C */ class addition; class multiplication; class equality; class constant; using node_ptr = std::unique_ptr< node >; /* C */ node_ptr read( std::string_view expr ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹json›] The goal of this exercise is to implement a printer for JSON, invoked as a ‹print› method available on each ‹node›. It should take no arguments and return an instance of ‹std::string›. For simplicity, the only scalar values you need to implement are integers. Then there are 2 composite data types: • arrays, which represent a sequence of arbitrary values, • objects, which map strings to arbitrary values. Both composite types are «heterogeneous» (the items can be of different types). They are formatted as follows: • array: ‹[ 1, [ 2, 3 ], 4 ]›, • object: ‹{ "key₁": 7, "key₂": [ 1, 2 ] }›. To further simplify matters, we will not deal with line breaks or indentation – format everything as a single line. class node; /* C */ using node_ptr = std::unique_ptr< node >; using node_ref = const node &; The ‹number› class is to be constructed from an ‹int›, has no children, and needs no methods besides ‹print›. class number; /* C */ The ‹object› and ‹array› classes represent composite JSON data. They should be both default-constructible, resulting in an empty collection. Both should have an ‹append› method: for ‹object›, it takes an ‹std::string› (the key) and a ‹node_ptr›, while for ‹array›, only the latter. In both cases, print items in the order in which they were appended. Duplicated keys are ignored (i.e. first occurrence wins). class object; /* C */ class array; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹while›] Consider an abstract syntax tree of a very simple imperative programming language with structured control flow. Let there be 3 types of statements: 1. a variable increment, ‹a ++›, 2. a while loop of the form ‹while (a != b) stmt›, and finally 3. a block, which is a sequence of statements. class statement; /* C */ using stmt_ptr = std::unique_ptr< statement >; We will represent variables as strings. Provide an ‹eval› method which takes variable assignment as an argument and returns the same as a result. Variables used in the program but not given in the input start as 0. The variable assignment (i.e. the «state» of the program) is represented as an ‹std::map› from strings to integers. using state = std::map< std::string, int >; /* C */ The constructors should be as follows: • ‹stmt_inc› takes the name of the variable to be incremented, • ‹stmt_while› takes 2 variable names and an ‹stmt_ptr› for the body, and • ‹stmt_block› is default-constructed as a noop, but provides an ‹append› method to insert a ‹stmt_ptr› at the end. class stmt_inc; /* C */ class stmt_while; class stmt_block; # T.2. Tasks with Operators, Exceptions and OOP The programming tasks for this block are as follows: 1. ‹machine.*› – a simple register machine simulator, 2. ‹natural.*› – arbitrary-size natural numbers, 3. ‹parser.*› – parsing simplified JSON, 4. ‹complex.*› – arbitrary-precision complex numbers. The first task only relies on knowledge from the first block and you should be able to start working on it immediately. Tasks 2 and 4 additionally require operator overloading (chapter 5) and basic understanding of exceptions (chapter 6). Finally, task 3 needs ‹unique_ptr› (chapter 7) and virtual dispatch (chapter 8), but the parser itself (and hence ‹xml_validate›) can be implemented with knowledge from block 1 alone, so you can still start early. ## [‹complex›] In this exercise, you will implement exact (arbitrary-precision) real and complex numbers. You can use the ‹natural› task as a base, if you wish. Both real and complex numbers should provide the standard array of arithmetic operators: addition, subtraction, unary minus, multiplication and division. Real numbers should have all relational operators and complex numbers should have equality (‹==› and ‹!=›). Note: keep your representation normalised – complexity of operations should only depend on the represented number, not on the way it was obtained. // extra files: natural.hpp natural.cpp /* C */ class real /* C */ { public: explicit real( int v = 0 ); real abs() const; real reciprocal() const; real power( int n ) const; }; class complex /* C */ { static inline const real epsilon = real( 1 ) / real( 1000000 ); public: explicit complex( real real_part = real( 0 ), real imaginary_part = real( 0 ) ); real real_part(); /* C */ real imaginary_part(); complex reciprocal() const; /* C */ complex power( int n ) const; Compute the: • exponential function ⟦exp(z)⟧, • the natural logarithm ⟦ln(1 + z)⟧, where ⟦z⟧ is ‹this›. Use the respective Taylor expansions at 0 (i.e. the Maclaurin series). The number of terms to use is given by ‹terms›. complex exp( int terms ) const; /* C */ complex log1p( int terms ) const; Compute the absolute value of the given complex number to the given precision (the argument ‹prec› gives the upper bound on the admissible approximation error). You may find the ‹newton› demo from week 2 helpful to compute ‹abs›. Don't forget to find a suitable starting point for the approximation, otherwise convergence will be very slow. real abs( real prec = epsilon ) const; /* C */ To compute the argument, you will need the inverse tangent (‹atan›), which can be approximated using its Maclaurin series in the closed interval ⟦⟨-1, 1⟩⟧. There is only one problem: the convergence near ⟦±1⟧ is very slow. Hence, you want to use a different series here (discovered by Euler): ⟦ ∑ 2²ⁿ(n!)²x²ⁿ⁺¹ / (2n + 1)!(1 + x²)ⁿ⁺¹ ⟧ Though this one will eventually converge everywhere, it is particularly good in the same ⟦⟨-1, 1⟩⟧ interval. In this interval, it can be truncated at the first term less than half the required precision. Now note that for any given ⟦x⟧, either ⟦x⟧ or ⟦1/x⟧ falls into ⟦⟨-1, 1⟩⟧: hence, you can use the reciprocal formula (‹atan(1/x)› is ‹2*atan(1) - atan(x)›) to find an expression for the argument which always falls into the interval of (fast) convergence. Don't forget that adding two numbers each with error ⟦≤ ε⟧ only guarantees that the sum has an error ⟦≤ 2ε⟧. Likewise, multiplication by an exact constant also multiplies the error. real arg( real prec = epsilon ) const; /* C */ Compute the exponential and ‹log1p› to a given precision. Assume that ⟦z⟧ (‹this›) is in the area of convergence for the required power series (the open unit disc for ‹log1p›, the entire complex plane for ‹exp›). Tip: to judge the precision, use the norm (square of the modulus), «not» the modulus itself. For ‹exp›, depending on the argument, the terms of the power series may grow before they start to shrink. Once they start to shrink and their norm falls below ‹prec› squared, you have achieved the required precision. How things work out with ‹log1p› is left as an exercise (it's much simpler). complex exp( real prec = epsilon ) const; /* C */ complex log1p( real prec = epsilon ) const; }; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ## [‹machine›] In this task, you will implement a simple register machine (i.e. a simple model of a computer). The machine has an arbitrary number of integer registers and byte-addressed memory. Registers are indexed from 1 to ‹INT_MAX›. Each instruction takes 2 register numbers (indices) and an ‘immediate’ value (an integral constant). Each register can hold a single value of type ‹int32_t› (i.e. the size of the machine word is 4 bytes). In memory, words are stored LSB-first. The entire memory and all registers start out as 0. The machine has the following instructions (whenever ‹reg_x› is used in the «description», it means the register itself (its value or storage location), not its index; the opposite holds in the column ‹reg_2› which always refers to the register index). │ opcode │‹reg_2›│ description │ ├────────┼───────┼─────────────────────────────────────────────┤ │ ‹mov› │ ≥ 1 │ copy a value from ‹reg_2› to ‹reg_1› │ │ │ = 0 │ set ‹reg_1› to ‹immediate› │ │ ‹add› │ ≥ 1 │ store ‹reg_1 + reg_2› in ‹reg_1› │ │ │ = 0 │ add ‹immediate› to ‹reg_1› │ │ ‹mul› │ ≥ 1 │ store ‹reg_1 * reg_2› in ‹reg_1› │ │ ‹jmp› │ = 0 │ jump to the address stored in ‹reg_1› │ │ │ ≥ 1 │ jump to ‹reg_1› if ‹reg_2› is nonzero │ │ ‹load› │ ≥ 1 │ copy value from addr. ‹reg_2› into ‹reg_1› │ │ ‹stor› │ ≥ 1 │ copy ‹reg_1› to memory address ‹reg_2› │ │ ‹halt› │ = 0 │ halt the machine with result ‹reg_1› │ │ │ ≥ 1 │ same, but only if ‹reg_2› is nonzero │ Each instruction is stored in memory as 4 words (addresses increase from left to right). Executing a non-jump instruction increments the program counter by 4 words. ┌────────┬───────────┬───────┬───────┐ │ opcode │ immediate │ reg_1 │ reg_2 │ └────────┴───────────┴───────┴───────┘ enum class opcode { mov, add, mul, jmp, load, stor, hlt }; /* C */ class machine /* C */ { public: Read and write memory, one word at a time. int32_t get( int32_t addr ) const; /* C */ void set( int32_t addr, int32_t v ); Start executing the program, starting from address zero. Return the value of ‹reg_1› given to the ‹hlt› instruction which halted the computation. int32_t run(); /* C */ }; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ## [‹natural›] In this task, you will implement a class which represents arbitrary-size natural numbers (including 0). In addition to the methods prescribed below, the class must support the following: • arithmetic operators ‹+›, ‹-›, ‹*›, ‹/› and ‹%› (the last two implementing division and reminder), • all relational operators, • bitwise operators ‹^› (xor), ‹&› (and) and ‹|› (or). The usual preconditions apply (divisors are not 0, the second operand of subtraction is not greater than the first). class natural /* C */ { public: Construct a natural number, optionally from an integer. Throw ‹std::out_of_range› if ‹v› is negative. explicit natural( int v = 0 ); /* C */ natural power( natural exponent ) const; /* C */ natural digit_count( natural base ) const; natural digit_sum( natural base ) const; }; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ## [‹parser›] Write a parser for simplified JSON: in our version, object keys are barewords (i.e. there is no escaping to deal with) and values are integers, arrays or objects (no strings or floats). The EBNF: (* toplevel elements *) value = blank, ( integer | array | object ), blank ; integer = [ '-' ], digits | '0' ; array = '[', valist, ']' | '[]' ; object = '{', kvlist, '}' | '{}' ; (* compound data *) valist = value, { ',', value } ; kvlist = kvpair, { ',', kvpair } ; kvpair = blank, key, blank, ':', value ; (* lexemes *) digits = nonzero, { digit } ; nonzero = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ; digit = '0' | nonzero ; key = keychar, { keychar } ; keychar = ? ASCII upper- or lower-case alphabetical character ? ; blank = { ? ASCII space, tab or newline character ? } ; It is okay to use ‹std::isspace› to implement the ‹blank› nonterminal. The interface should be as follows: class json_value; /* C */ using json_ptr = std::unique_ptr< json_value >; using json_ref = const json_value &; enum class json_type { integer, array, object }; /* C */ class json_error /* C */ { public: const char *what() const; }; class json_value /* C */ { public: virtual json_type type() const = 0; virtual int int_value() const = 0; virtual json_ref item_at( int ) const = 0; virtual json_ref item_at( const std::string & ) const = 0; virtual int length() const = 0; virtual ~json_value(); }; Semantic requirements: • when a string that is passed to ‹json_parse› does not conform to the above grammar, the function should throw an exception of type ‹json_error›, • a duplicated key within an object should throw ‹json_error› too, • the ‹item_at› overloads should throw an instance of ‹std::out_of_range› when the specified element does not exist, • the ‹item_at› overload which accepts an integer, when called on a value of type ‹json_type::object›, should return elements in key-alphabetical order, • ‹length› should never throw (integers should have a length of 0). Check that the input document is well-formed (i.e. it conforms to the grammar). Return ‹true› or ‹false› depending on the outcome. Do not throw any exceptions. bool json_validate( const std::string & ); /* C */ Read a simplified-JSON document. Throw ‹json_error› if the document is ill-formed. json_ptr json_parse( const std::string & ); /* C */ # Templates You have hopefully already noticed that certain classes in the C++ standard library are «parametric»: they can be «instantiated» with different «type parameters»: that is, we can create an ‹std::vector› of integers, but we can also create an ‹std::vector› of floating-point numbers. Even more interestingly, we can create an ‹std::vector› of instances of our own classes. It is probably quite clear that there is a single entity called ‹std::vector›: this entity is called a «class template». Unfortunately, the terminology gets slightly confusing here: the «instances» of «class templates» are «classes». This is different from «objects» which are also known as «class instances». Demonstrations: 1. ‹zipper› – our favourite data structure, now generic 2. ‹expr› – a different take on expressions 3. ‹fold› – function templates 4. ‹rel› – non-type template arguments Elementary exercises: 1. ‹iota› – generate an integer sequence 2. ‹quot› – quotient fields (aka fields of fractions) 3. ‹split› – slice a string view into two on a delimiter Preparatory exercises: 1. ‹circular› – a circular list, but generic 2. ‹buffer› – a fixed-size queue-like data structure 3. ‹stats› – median, quartiles, mode over any container 4. ‹sparse› – sparse copy-on-write arrays 5. ‹bbox› – a bounding box of a point collection 6. ‹visit› – call a function on each node of a graph Regular exercises: 1. ‹tfold› – fold a tree using an arbitrary function 2. ‹tmap› – apply a function to each node 3. ‹monoid› – free monoids and homomorphisms 4. ‹treap› † – a combination of a heap and a binary search tree 5. ‹critbit› – binary tries with generic values 6. ‹finally› – a generic RAII wrapper ## d. Demonstrations ### [‹zipper›] The canonic use for C++ templates is designing container classes (collections) which can hold different types of values. In C, the solution was to either use ‹void› pointers (e.g. linked lists in PB071), implement data structures using macros (e.g. linked lists in the Linux kernel), or remember store sizes explicitly and copy in data using ‹void› pointers and the element size (e.g. ‹glib› data structures). Let's now look at the C++ way, using the familiar ‹zipper› as an example. You may find it helpful to compare this example with ‹05/access.cpp›, which implemented a non-generic version of the same class. Definition of a «class template» looks the same as a definition of a normal class, all we need to do is specify that we want a template instead, with a single type parameter, which is, by convention, called ‹T›: template< typename T > /* C */ We can now proceed to define a ‹class› and it will become a «class template» (i.e. it will be parametrized by ‹T› above). Anywhere in the body of the class where we can specify a type, we can now also use ‹T›. When we instantiate the template later, the compiler will find all occurrences of ‹T› in the class definition and replace them with the supplied type parameter. This process is known as «substitution». class zipper /* C */ { If you remember from a few weeks ago, a zipper can be represented using 2 stacks. Like before, we will use a pair of ‹std::vector› instances. However, the zipper now stores values of type ‹T›, so we will supply that as the type parameter of ‹vector›. using stack = std::vector< T >; /* C */ stack left, right; T focus; public: /* C */ Forwarding arguments of arbitrary types is a little too advanced for us, so we will settle for making a copy of the initial value. This means that we require ‹T› to be a type with a copy constructor. In other words, we won't be able to create zippers that hold values of type ‹unique_ptr›. zipper( const T &f ) : focus( f ) {} /* C */ Like above, we will settle for a copy. zipper &push_left( const T &x ) /* C */ { left.push_back( x ); return *this; } zipper &push_right( const T &x ) /* C */ { right.push_back( x ); return *this; } The ‹shift› helper remains unchanged from our previous implementation. void shift( stack &a, stack &b ) /* C */ { b.push_back( focus ); focus = a.back(); a.pop_back(); } This time, we will only have pre-increment and pre-decrement, since those are the only practical variants for this class. zipper &operator++() { shift( right, left ); return *this; } /* C */ zipper &operator--() { shift( left, right ); return *this; } The dereference and indirect access operators are more interesting, since they need to mention the element type, which is now ‹T›. T &operator*() { return focus; } /* C */ T *operator->() { return &focus; } And the ‹const› overloads of the same: const T &operator*() const { return focus; } /* C */ const T *operator->() const { return &focus; } Indexing operator (non-‹const› overload only). T &operator[]( int i ) /* C */ { if ( i == 0 ) return focus; if ( i < 0 ) return left[ left.size() + i ]; if ( i > 0 ) return right[ right.size() - i ]; assert( false ); } }; int main() /* demo */ /* C */ { Let's first create a ‹zipper› which holds integers. zipper< int > zi( 0 ); // [0] /* C */ zi.push_left( 2 ); // 2 [0] zi.push_left( 1 ); // 2 1 [0] zi.push_right( 1 ); // 2 1 [0] 1 check assert( zi[ -2 ] == 2 ); /* C */ assert( zi[ -1 ] == 1 ); assert( zi[ 0 ] == 0 ); assert( zi[ 1 ] == 1 ); assert( *zi == 0 ); /* C */ And now a different instance, with pairs. using p = std::pair< int, int >; /* C */ zipper< p > zp( p( 0, 1 ) ); assert( *zp == p( 0, 1 ) ); /* C */ assert( zp->first == 0 ); assert( zp->second == 1 ); zp.push_left( p( 7, 7 ) ); /* C */ assert( zp[ -1 ] == p( 7, 7 ) ); -- zp; /* C */ assert( zp->first == 7 ); assert( zp->second == 7 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹expr›] We will do another take at expressions, this time with templates, which will allow us to use values instead of references. While the first two examples were directly comparable to earlier versions, this one will deviate quite far from ‹07/expr.cpp›, though you may still find it useful to quickly go over that one first. The semantics will be the same: we will have sums, products and constants. However, there will be no distinction between static and dynamic types and no ‹virtual› methods. We will start with constants, since those are the simplest. Since we are not using ‹virtual› methods, we also don't need a common base class or inheritance. We do, however, need to provide a common interface (i.e. method names and signatures) between the different classes. class constant /* C */ { int _value; public: constant( int v ) : _value( v ) {} int value() const { return _value; } }; So far, we have only seen templates with a single type parameter. However, we can have as many as we want (in fact, we can even have a variable number, though that is outside of the scope of this subject). For ‹sum›, we will need 2: the type of the left and of the right sub-expression. template< typename left_t, typename right_t > /* C */ class sum { Unlike before, the «static» type of the left and the right sub-expressions may be different, and instead of references or pointers, we will simply store them by value in attributes. left_t _left; /* C */ right_t _right; public: /* C */ We need to define a constructor. Like in our earlier take on expressions, the constructor will take the two sub-expressions as arguments. This time, their types are given by the template parameters though. Other than that, the constructor is pretty normal. sum( const left_t &l, const right_t &r ) /* C */ : _left( l ), _right( r ) {} And the interface to get values out of the expression: int value() const { return _left.value() + _right.value(); } /* C */ }; The ‹product› class looks pretty much the same: template< typename left_t, typename right_t > /* C */ class product { left_t _left; right_t _right; public: /* C */ product( const left_t &l, const right_t &r ) /* C */ : _left( l ), _right( r ) {} int value() const { return _left.value() * _right.value(); } /* C */ }; The duplication is somewhat unsatisfactory. Maybe we could do a little better by using inheritance, so let's try defining another class. First the base class: template< typename left_t, typename right_t > /* C */ class operation { protected: left_t _left; right_t _right; public: /* C */ operation( const left_t &l, const right_t &r ) : _left( l ), _right( r ) {} }; Now a derived class -- let's do subtraction. Remember that inheritance works with classes, but ‹operation› is a class ‹template›: we need to instantiate it to obtain a class before we can inherit from it! template< typename left_t, typename right_t > /* C */ class difference : public operation< left_t, right_t > { public: difference( const left_t &l, const right_t &r ) : operation< left_t, right_t >( l, r ) {} Plot twist: if the type of the base class depends on template parameters, we cannot directly access inherited attributes. Instead, we have to explicitly tell the compiler that those are attributes of this class using ‹this›. int value() const /* C */ { return this->_left.value() - this->_right.value(); } }; That wasn't much better. Templates are, unfortunately, somewhat verbose. On the upside, notice that we have implemented the first two operations (‹+›, ‹*›) somewhat differently from the last (‹-›), but they can still interoperate smoothly. Templates use «duck typing»: if it looks and quacks like a duck (i.e. it has the right attributes and methods) it probably is a duck, and the compiler will let us use the type with the template. int main() /* demo */ /* C */ { We first define some constants, those are simple. constant c_0( 0 ), c_1( 1 ), c_2( 2 ); /* C */ When we create instances of class templates using constructors which take arguments of types that match the type parameters of the template, we do not need to explicitly type them out. This is the same principle that lets us write ‹std::pair( 0, 1 )›. The feature is called «template argument deduction» and we will see more of it with function templates in the next unit. Of course, we can specify the template arguments ourselves if we want to, but it gets tedious rather quickly. We will show both styles, first the explicit one: sum< constant, constant > s_1( c_0, c_1 ); /* C */ sum< sum< constant, constant >, constant > s_2( s_1, c_1 ); This is clearly impractical. Let's try the implicit style. sum s_3( s_2, c_1 ); /* C */ product p_0( c_0, c_1 ); product p_1( c_1, s_1 ); That is much better. Let's make some differences and then we will check all the values. difference d_2( s_3, s_1 ); /* C */ difference d_0( d_2, c_2 ); assert( c_0.value() == 0 ); /* C */ assert( s_1.value() == 1 ); assert( s_2.value() == 2 ); assert( s_3.value() == 3 ); assert( p_0.value() == 0 ); assert( p_1.value() == 1 ); assert( d_2.value() == 2 ); assert( d_0.value() == 0 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹fold›] In this unit, we will look at «function templates», which are similar to class templates we have seen in previous units. Function templates rely even more heavily on template parameter deduction than class templates: most of the time, calling function templates looks just like calling standard function: the compiler will «deduce» the type parameters from the actual argument types. We will see that later down. One further thing to note is that we have actually met function templates quite early on, we just did not mention they were templates: the call operator of a «lambda» with an ‹auto› parameter is, in fact, a function template, the only difference is that the syntax is (usually) less verbose. We will start by defining some commonly useful folds on containers. Let's start with ‹sum›. The container type will be a «type parameter»: we want our ‹sum› to work on different container types (for instance sets and vectors) and also with different «element types»: the types of items stored in those containers. The syntax for function templates is pretty much the same as it was for class templates: template< typename container_t > /* C */ followed by a standard function signature. In this case, we have a small problem, since we don't have a name for the type of the return value. For now, we can use ‹auto›. auto sum( const container_t &xs ) /* C */ { There are two ways to go about writing the summing loop, with different trade-offs in terms of types. Probably the most reasonable thing to do is to declare an accumulator of the correct type and initialize it to 0. For that, however, we need to know the type of the values stored in ‹xs›. Fortunately, standard library provides us with a way to do just that: each standard container has a «nested type name», which we can access using ‹::›. If the outer type is a template argument, or depends on a template argument, we additionally need to tell the compiler that we intend to refer to a type (since templates are «duck typed», the nested name could also turn out to be an attribute or a method). The type of a single element stored in a container is known as its ‹value_type›. using value_t = typename container_t::value_type; /* C */ Now that we have named the type of values in the container, we can declare an accumulator with the correct type. Again, by the virtue of duck typing, we do not know for certain whether values of this type can be constructed from integers, but we assume that they can. When we attempt to use the template, the compiler will check and emit errors if this is, in fact, not possible. value_t accum = 0; /* C */ The loop itself is then quite trivial. for ( const auto &x : xs ) /* C */ accum = accum + x; return accum; /* C */ } Let's also try to do a product, in a slightly different style, just to see some more options. In this case, since we do not make any use of ‹container_t›, it would be easier to simply use a lambda. We will do that in a bit. template< typename container_t > /* C */ auto product( const container_t &xs ) { auto accum = xs.empty() ? 1 : *xs.begin(); bool first = true; for ( const auto &x : xs ) /* C */ if ( first ) first = false; else accum = accum * x; return accum; /* C */ } Let us do ‹mean› in a «lambda» style, so we have a comparison at hand. We can re-use ‹sum› from above. We will take the average of an empty sequence to be 0. static auto mean = []( const auto &xs ) /* C */ { return xs.empty() ? 0 : sum( xs ) / xs.size(); }; Finally, we will generalize the two folds (sum and product), and add a ‹zip_with› for a good measure, in the ‹template› style. We can accept «functions as arguments» the same way we accept any other values. This will work with anything that can be called (remember «duck typing»?), most importantly lambdas. The initial value of the accumulator passed in by the user gives us the type of the accumulator ‘for free’. In practice, this is a little dangerous in the sense that it could give us some unexpected results if enough implicit conversions are allowed (like accumulating rational numbers into an integer). I will show you another version of ‹fold› as a bonus after we do ‹zip_with›. template< typename xs_t, typename fun_t, typename init_t > /* C */ auto fold( const xs_t &xs, const fun_t &f, const init_t &init ) { The fold itself is pretty trivial, once we have figured out the types. init_t accum = init; /* C */ for ( const auto &x : xs ) accum = f( accum, x ); return accum; } Now for the ‹zip_with›. It will accept two sequences and a function. The result will be a ‹vector›, since we do not have a good way to tell the function what type of a container we would like. template< typename xs_t, typename ys_t, typename fun_t > /* C */ auto zip_with( const xs_t &xs, const ys_t &ys, const fun_t &f ) { We need a new trick, because there is nothing at hand that would give us the element type of the result (even if we settled for a ‹vector› as the container). The way to find out is ‹decltype›, an operator that takes an «expression» and produces its «type». Whenever we can write out a name of a type, we can instead use ‹decltype› with an expression. The expression must only refer to names that are in scope at the point of the ‹decltype› though. using value_t = decltype( f( *xs.begin(), *ys.begin() ) ); /* C */ Note: there is a bit of a danger in the above: this function will not work with an ‹f› that returns a reference. Repairing this deficiency is beyond the scope of this course. Ask if you are interested though. std::vector< value_t > out; /* C */ We want our ‹zip_with› to terminate when it hits the end of the shorter sequence. This means we cannot use ‹std::transform›, unfortunately, so we will type out the loop by hand. auto x = xs.begin(); /* C */ auto y = ys.begin(); while ( x != xs.end() && y != ys.end() ) /* C */ out.push_back( f( *x++, *y++ ) ); return out; /* C */ } And finally, the promised ‘bonus’ ‹fold›, which prefers the return type of ‹f› as its accumulator type. We have the basic recipe for that in ‹zip_with›. template< typename xs_t, typename fun_t, typename init_t > /* C */ auto fold_( const xs_t &xs, const fun_t &f, const init_t &init ) { using accum_t = decltype( f( init, *xs.begin() ) ); Note that ‹accum_t› and ‹init_t› may be different types. accum_t accum = init; /* C */ for ( const auto &x : xs ) /* C */ accum = f( accum, x ); return accum; /* C */ } For a good measure, we will define a custom class of numbers. You might remember ‹rat› from an earlier exercise. The minimum viable definition follows. struct rat /* C */ { int p, q; rat( int p, int q = 1 ) : p( p ), q( q ) {} friend rat operator+( rat r, rat s ) /* C */ { return { r.p * s.q + s.p * r.q, r.q * s.q }; } rat operator*( rat r ) const { return { p * r.p, q * r.q }; } /* C */ rat operator/( rat r ) const { return { p * r.q, q * r.p }; } bool operator<( rat r ) const { return p * r.q < r.p * q; } /* C */ bool operator==( rat r ) const { return p * r.q == r.p * q; } }; int main() /* demo */ /* C */ { std::set< int > xs{ 1, 2, 3 }; std::vector< double > ys{ 1.5, 2 }; std::set< rat > zs{ 1, { 1, 2 }, { 1, 4 } }; The only interesting thing in the below test cases is that the functions are used like standard functions: no angle brackets to be seen anywhere. This is because the compiler «deduces» the type parameters from the types of the actual arguments which we pass into the function. Since all template arguments can be deduced this way, we can omit angle brackets entirely. assert( sum( xs ) == 6 ); /* C */ assert( sum( ys ) == 3.5 ); assert( sum( zs ) == rat( 7, 4 ) ); assert( product( xs ) == 6 ); /* C */ assert( product( ys ) == 3 ); assert( product( zs ) == rat( 1, 8 ) ); assert( mean( xs ) == 2 ); /* C */ assert( mean( ys ) == 1.75 ); assert( mean( zs ) == rat( 7, 12 ) ); When calling our original ‹fold›, we have to be careful with the type of the initial value, otherwise we will run into problems. This is somewhat inconvenient. assert( fold( zs, std::plus<>(), rat( 0 ) ) == rat( 7, 4 ) ); /* C */ On the other hand, our improved version (here named ‹fold_›) works just fine if we write it in a ‘natural’ style. assert( fold_( zs, std::plus<>(), 0 ) == rat( 7, 4 ) ); /* C */ Finally, let's look at ‹zip_with›. std::vector xs_ys{ 2.5, 4.0 }; /* C */ The sets are «sorted» in ascending order, so the pairings will be 1/4 + 1, 1/2 + 2 and 1 + 3. std::vector xs_zs{ rat( 5, 4 ), rat( 5, 2 ), rat( 4 ) }; /* C */ assert( zip_with( xs, ys, std::plus<>() ) == xs_ys ); /* C */ assert( zip_with( xs, zs, std::plus<>() ) == xs_zs ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹rel›] We will take a second look at function templates, but this time we will also add non-type template arguments to the mix. We haven't used it much, but this is how ‹std::get› works. It might be useful to review ‹03/p4_rel.cpp› before diving into this example. General projections are still too hard for us, so we will only do a single-column projection. However, selection is easier so let's look at those first. We will need 3 template arguments: one to specify which column to use as the selection criterion, another to specify the type of a single row and the last one to specify the type of the value which we will compare with the entries. template< int index, typename rel_t, typename key_t > /* C */ rel_t select( const rel_t &rel, const key_t &key ) { Since the type of the relation does not change under selection, it is simple enough to create an empty relation and add matching rows from ‹rel› to it. rel_t out; /* C */ We assume that it is possible to iterate a ‹rel_t›, and that it is possible to ‹insert› things into a ‹rel_t›. Since templates are «duck-typed», this will be checked when the template is instantiated. for ( const auto &row : rel ) /* C */ We now need to decide whether the row matches the criterion: the ‹index›-th column should be equal to ‹key› for that, so let's check that. if ( std::get< index >( row ) == key ) /* C */ Just insert the row. out.insert( row ); /* C */ And return the result. return out; /* C */ } Now for a single-column projection. Again, we will pass in ‹index› as a template parameter. However, we will run into some problems with the return type. Fortunately, in the signature, we can just use ‹auto› as the return type and worry about it later. template< int index, typename rel_t > /* C */ auto project( const rel_t &rel ) { Actually, we can't put that off for very long. We need to declare the variable to hold the resulting relation. First of all, we need to find out the type of a single row. For that, we can use standardized nested types that all ‹std› containers provide, which we have learned about in the previous unit. The row is the ‹value_type› of the relation, like this. Remember that the ‹typename› specifier is compulsory whenever we want to refer to a type nested within a template parameter, or within something that depends on a template parameter. using row_t = typename rel_t::value_type; /* C */ Now that we have the row type, we need to extract the type of ‹index›-th column. For that, the standard library provides the ‹tuple_element_t› helper template, like this: using col_t = std::tuple_element_t< index, row_t >; /* C */ Now we have the type that we need to construct the output relation, which we will construct as a set of ‹col_t›. std::set< col_t > out; /* C */ At this point, the code for ‹project› is just a variation on what we already saw in ‹select›. for ( const auto &row : rel ) /* C */ out.insert( std::get< index >( row ) ); return out; /* C */ } int main() /* demo */ /* C */ { using element = std::tuple< std::string, int, double >; using elem_rel = std::set< element >; We first define a testing data set. elem_rel r{ { "hydrogen", 1, 0.78 }, /* C */ { "hydrogen", 2, 1.50 }, { "hydrogen", 3, 3.09 }, { "helium", 3, 3.09 }, { "iron", 56, 9.15 }, { "iron", 58, 9.14 }, { "nickel", 60, 9.15 }, { "nickel", 62, 9.15 } }; Using ‹select› is straightforward: we specify, as a template parameter (i.e. using angle brackets) the column index, and pass in the relation and the key as standard arguments. The types of the relation and the key are then deduced automatically. You may find it helpful to compare the calls with the definition of ‹select› above. elem_rel nickel = select< 0 >( r, "nickel" ), /* C */ iron = select< 0 >( r, "iron" ), helium = select< 0 >( r, "helium" ); elem_rel helium_expect{ { "helium", 3, 3.09 } }, /* C */ iron_expect { { "iron", 56, 9.15 }, { "iron", 58, 9.14 } }, nickel_expect{ { "nickel", 60, 9.15 }, { "nickel", 62, 9.15 } }; assert( helium == helium_expect ); /* C */ assert( iron == iron_expect ); assert( nickel == nickel_expect ); Now for projection: again, we explicitly specify the index of the column to extract, as a template parameter. The type of the relation is deduced and we therefore do not need to mention it. auto names = project< 0 >( r ); /* C */ std::set< std::string > names_expect{ "hydrogen", "helium", "iron", "nickel" }; assert( names == names_expect ); /* C */ std::set< std::pair< int, int > > p{ { 1, 1 }, { 1, 2 }, /* C */ { 2, 2 }, { 2, 4 } }; std::set< int > left{ 1, 2 }, right{ 1, 2, 4 }; /* C */ assert( project< 0 >( p ) == left ); /* C */ assert( project< 1 >( p ) == right ); assert( project< 1 >( select< 0 >( p, 1 ) ) == left ); } ## e. Elementary Exercises ### [‹iota›] Implement a generic function ‹iota›, which, given a function ‹f›, calls ‹f( start )›, ‹f( start + 1 )›, … ‹f( end - 1 )›, in this order. // void iota( … f, int start, int end ); /* C */ ### [‹quot›] A quotient field is a generalization of rational numbers: one can be constructed from any «integral domain». When the integral domain is taken to be Z (the integers), the result is Q (the rational numbers). However, the construction is much more general and can be applied to polynomials, Gaussian integers, p-adic numbers and so on. Here, we will construct standard rationals and Gaussian rationals (which are like normal rationals, but with an imaginary part). Define a «class template» ‹rat›. The type parameter will provide the integral domain: ‹int› for integers, ‹gauss› for Gaussian integers. The constructor should take the numerator and the denominator as arguments. Define addition, multiplication and division on ‹rat›'s, as well as equality. When done, implement ‹gauss›, which is simply a complex number where both the real and imaginary parts are integers. Store them in algebraic form for simplicity. Define addition, multiplication and equality. ### [‹split›] Implement a function ‹split›, which given a string view ‹s› and a delimiter ‹delim›, produces a pair of string_views ‹a› and ‹b› such that: • ‹delim› is not in ‹a›, • and either ◦ ‹s == a + delim + b› if ‹delim› was present in ‹s›, ◦ ‹s == a› and ‹b› is empty otherwise using split_view = std::pair< std::string_view, std::string_view >; /* C */ split_view split( std::string_view s, char delim ); ## p. Preparatory Exercises ### [‹circular›] In this exercise, we will implement a circular list again, but this time generically, i.e. using templates. Like before, instead of the usual access operators and iteration, it will have a ‹rotate› method, which rotates the entire list. We require that rotation does not invalidate any references to elements in the list. If you think of the list as a stack, you can think of the ‹rotate› operation as taking an element off the top and putting it at the bottom of the stack. It is undefined on an empty list. To add and remove elements, we will implement ‹push› and ‹pop› which work in a stack-like manner. Only the top element is accessible, via the ‹top› method. This method should allow both read and write access. Finally, we also want to be able to check whether the list is ‹empty›. It is okay to make copies in ‹push›, but make sure you return references in ‹top›. template< typename T > /* C */ struct circular_node; /* ref: 8 lines */ template< typename > /* C */ class circular; /* ref: 34 lines */ ### [‹buffer›] We will implement another data structure. We have not demonstrated the use of non-type template parameters with class template, but the principle is the same as it was in function templates in ‹d4_rel.cpp›. An additional hint: the size of an ‹std::array› is a non-type template argument (of type ‹size_t›). Implement a bounded circular buffer with a fixed size, as a class template with a single type argument ‹T› (which comes first) and a single non-type argument ‹size› of type ‹size_t› (which comes second). The class should be default-constructible and it can assume that ‹T› is also default-constructible and that it can be copied. The circular buffer should provide the following methods: • ‹push› inserts a value at one ‘end’ • ‹pop› removes and returns a value from the other ‘end’ • ‹empty› which returns true if there are no items • ‹full› if there are already ‹size› items Calling ‹push› on a full and ‹pop› on an empty buffer is undefined. Pushing new items should wrap around the end of the storage and start re-using storage from the start, as long as ‹pop› has been called in the meantime (i.e. the buffer is not full). In other words, ‹buffer› with ‹push› and ‹pop› behave like a FIFO queue which can hold at most ‹size› elements. template< typename T, size_t size > /* C */ class buffer; /* ref: 23 lines */ ### [‹stats›] In this exercise, we will do some basic statistics: median, quartiles and mode. Implement the functions ‹mode›, ‹median› and ‹quartiles›, in such a way that it accepts any sequential ‹std› container, with element type that allows less-than and equality comparison. Additional notes: • ‹mode› returns an ‹std::set› of numbers, since there might be arbitrarily many: include any input number for which the number of occurrences is maximal • ‹median› return a single number; pick the smaller of the two elements if the median lies in between two different numbers • ‹quartiles› returns numbers at indices (size / 4) and ( ( 3 * size ) / 4 ) of the sorted input sequence, in an ‹std::pair› [this is slightly incorrect but simpler]. // mode: ref. 15 lines /* C */ // median: ref. 7 lines // quartiles: ref. 8 lines ### [‹sparse›] Imagine there is a large array of data (e.g. numbers), but we sometimes need to change a few of those. However, we also need to keep the original data intact, and we don't want to copy all the data around. In this exercise, we will design a simple solution to this problem. Implement class template ‹sparse›, with a type argument ‹T› and a ‹size_t› argument ‹N›, with the following interface: • construct from an ‹std::array› of ‹T› with size ‹N›, • construct a copy (the ‹array› given to the constructor is «shared» by all copies), • ‹set( i, v )› replaces the value stored at index ‹i› with ‹v› (without affecting the backing array), • ‹get( i )› returns the value at index ‹i› (that is the ‹v› passed to latest call to ‹set( i, v )› with the same ‹i›, or the value from the backing array if ‹set› was never called for ‹i›, • ‹merge()› that propagates the changes made in this instance into all other instances sharing the same backing array. Note: the memory complexity should be O(N) of shared data, and O(M) per instance of ‹sparse› where M is the number of altered indices. A ‹merge› on one copy should «not» affect altered indices in other copies. template< typename T, size_t N > /* C */ class sparse; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹bbox›] We will dust off geometry a little bit: we will look at constructing a bounding box around a sequence of points (this time in 3D). Points can be constructed from three floating-point numbers (of type ‹double›. struct point; /* C */ There is a ‹dist› function which gives the Euclidean distance of two points. double dist( point a, point b ); /* C */ A helper function to check approximate point equality. bool close( point a, point b ) { return dist( a, b ) < 1e-10; } /* C */ Now for the bounding box: we want an axis-aligned box (i.e. not the smallest one), and will represent it using 2 points -- those in the opposite corners. Some of the resulting dimensions might be 0 (in case the points all lie on a line or in a plane). Return the points in such a way that the coordinates of the first one are smaller along all axes. It should be possible to pass the points using a ‹const› reference to any container which can be iterated. using box_t = std::pair< point, point >; /* C */ // ... box_t box( ... ) ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹visit›] The input graph is given using adjacency lists: the ‹graph› type gives the successors for each vertex present in the graph. template< typename vertex_t > /* C */ using graph = std::map< vertex_t, std::vector< vertex_t > >; Visit each vertex of graph ‹g› reachable from ‹initial› once and call ‹f› on its value. The order of calls is not important. // void visit( … g, … f, … initial ); /* C */ ## r. Regular Exercises ### [‹tfold›] Fold a proper binary tree using an associative and commutative binary function (proper meaning that each node either has both children, or none). template< typename value_t > /* C */ struct tree { std::unique_ptr< tree > left, right; value_t value; static auto make_tree( const tree &t ) /* C */ { return std::make_unique< tree >( t ); } tree( const tree &t ) /* C */ : left( t.left ? make_tree( *t.left ) : nullptr ), right( t.right ? make_tree( *t.right ) : nullptr ), value( t.value ) {} tree( value_t value, const tree &l, const tree &r ) /* C */ : left( make_tree( l ) ), right( make_tree( r ) ), value( std::move( value ) ) {} tree( value_t value ) : value( std::move( value ) ) {} /* C */ }; Given a binary function ‹f› and a tree instance ‹t›, compute a single value that is the result of folding the entire tree. Since ‹f› is both associative and commutative, it does not matter in which order you combine the individual values. // … tfold( … f, … t ) /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹tmap›] Goal: build a tree by preserving the structure of an existing tree, but obtain new values by applying a given function to the originals. The type of the value may change. Hint: assuming • ‹function_t fun› is a unary function, • ‹value_t val› is a value such that ‹fun( val )› makes sense, you can use: • ‹std::invoke_result_t< function_t, value_t >› to obtain the type of ‹fun( val )› without having either ‹fun› or ‹val› (only their types). template< typename value_t > /* C */ struct tree { value_t value; std::vector< tree > children; tree( value_t v, std::vector< tree > ch = {} ) /* C */ : value( std::move( v ) ), children( std::move( ch ) ) {} }; Build a tree of a suitable type given a function ‹f› which maps values to values and some tree ‹t›, compatible with ‹f›. // … tmap( … f, … t ) /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹monoid›] Monoids are algebraic structures with a single operation, usually written as multiplication. A «free monoid» ⟦M⟧ on a set ⟦A⟧ is defined as the set of all strings of elements from ⟦A⟧ with «concatenation» as the operation. A monoid «homomorphism» is a map ⟦f⟧ from ⟦M⟧ to ⟦M'⟧ with the property ⟦f(a⋅b) = f(a)⋅f(b)⟧. All monoids arise as «homomorphic images» of a free monoid on some set. Define a «class template» ‹monoid›, which takes a single type argument, ‹hom_t›, with the following behaviour: • the constructor will then accept ‹hom›, a value (typically a lambda) of type ‹hom_t›, as an argument and store it in an attribute (by value), • method ‹elem›, which takes an ‹std::string› and returns a value of a suitable type with a multiplication and an equality operator. The class should work with a fixed underlying set: the minuscule Latin letters (i.e. 'a' through 'z') and use the mechanics of a free monoid to implement multiplication. The provided ‹hom› will take an ‹std::string› as an argument, and return a value of some arbitrary type. Assume applying ‹hom› to a string yields values which can be compared, but not multiplied (at least not in a way compatible with the provided homomorphism). template< typename hom_t > /* C */ class monoid_element; /* ref: 11 lines */ template< typename hom_t > /* C */ class monoid; /* ref: 8 lines */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹treap›] † A treap is a combination of a binary search tree and a binary heap. Of course, a single structure cannot be a heap and a search tree on the same value: • a search tree demands the value in the right child to be greater than the value in the root, • a max heap demands that the value in both children be smaller than the root (and hence specifically in the right child). Treap has therefore a «pair» of values in each node: a «key» and a «priority». The tree is arranged so that it is a binary search tree with respect to keys, and a binary heap with respect to priorities. The role of the heap part of the structure is to keep the tree approximately balanced. Your task is to implement the insertion algorithm which works as follows: 1. insert a new node into the tree, based on the key alone, as with a standard binary search tree, 2. if this violates the heap property, rotate the newly inserted node toward the root, until the heap property is restored. The deeper the node is inserted, the more likely it is to violate the heap property and the more likely it is to bubble up, causing the affected portion of the tree to be rebalanced by the rotations. Remember that rotations do not change the in-order of the tree and hence cannot disturb the search tree property. The type of the keys is given as a template parameter, while the priority should be kept as an ‹int›. Interface: • ‹insert( key, priority )› inserts the given values into the tree, maintaining both invariants, • ‹root()› returns a pointer to the root node (‹nullptr› if the tree is empty). Each node then provides the following: • ‹left()› and ‹right()› return raw pointers to the left and right child, or ‹nullptr› if the respective child is missing, • ‹priority()› and ‹key()› give access to the values stored in the node. template< typename key_t > /* C */ class treap; template< typename node_t > /* C */ void check_heap( const node_t &n ) { for ( auto child : { n.left(), n.right() } ) if ( child ) { assert( child->priority() <= n.priority() ); check_heap( *child ); } } template< typename node_t > /* C */ std::pair< int, int > check_search( const node_t *n, int bound ) { if ( !n ) return { bound, bound }; auto [ l_min, l_max ] = check_search( n->left(), n->key() ); /* C */ auto [ r_min, r_max ] = check_search( n->right(), n->key() ); assert( l_max <= n->key() && n->key() <= r_min ); /* C */ return { l_min, r_max }; } treap< int > make_treap( int count ) /* C */ { std::mt19937 rand( 0 ); std::uniform_int_distribution< int > dist( -10, 1000 ); treap< int > t; for ( int i = 0; i < count; ++ i ) /* C */ { t.insert( dist( rand ), dist( rand ) ); assert( t.root() ); check_search( t.root(), 0 ); check_heap( *t.root() ); } return t; /* C */ } template< typename node_t > /* C */ int get_depth( const node_t &n ) { int depth = 1; for ( auto child : { n.left(), n.right() } ) /* C */ if ( child ) depth = std::max( depth, get_depth( *child ) ); return depth; /* C */ } void check_sized( int size ) /* C */ { double depth_sum = 0; const int count = 100; for ( int i = 0; i < count; ++i ) /* C */ { auto t = make_treap( count ); depth_sum += get_depth( *t.root() ); } assert( depth_sum / count <= 2 * ( log2( size ) + 1 ) ); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹finally›] Unlike many other languages, there is no built-in ‹finally› in C++. In this exercise, we will implement a RAII class that provides similar functionality. The user should be able to write code like this: finally close_fd( []{ ::close( fd ); } ); While the syntax is not perfect, it presents some opportunities not normally available with ‹finally›. One is that the guard can be disabled (add a ‹cancel› method to do that) and possibly moved around (e.g. passed down to a tail call). Do note that there are pitfalls associated with this kind of object that we will not be addressing in this exercise. Most importantly, we will assume that the function passed into ‹finally› does not itself throw (if it does, things will go very badly). template< typename handler_t > /* C */ class finally; # Templates (cont'd) This week, we will practice templates some more, and introduce a few more useful template constructs. Among other things, we'll look at more complicated cases of template argument deduction and how function overloading interacts with function templates. We will also look at templated operator overloads. Demonstrations: 1. ‹apply› – call functions on scalars in composite data 2. ‹method› – method templates 3. ‹expr› – expressions, this time with operator overloading 4. ‹set› – operators on sets (with arbitrary element types) 5. ‹call› – overloading the call operator Elementary exercises: 1. ‹format› – format collections 2. ‹concat› – splice two sequences into one 3. ‹icons› – integer lists with compile-time recursion Preparatory exercises: 1. ‹post› – post-order on a generic graph 2. ‹cons› – heterogeneous lists 3. ‹map› – more template argument deduction 4. ‹collect› – extract values from containers 5. ‹list› – linked lists with sub-list sharing 6. ‹vecset› – implement a set using a sorted vector Regular exercises: 1. ‹select› – create a vector of variants 2. ‹sorted› – a stateful sequence observer 3. ‹fsm› – generic finite state machines 4. ‹tree› – trees with sub-tree sharing 5. ‹bimap› – map 2 types of keys to each other 6. ‹tinyvec› † – a generic vector in fixed memory ## d. Demonstrations ### [‹apply›] In this example, we will show how to use recursion together with templates to walk through composite, templated data types. In particular, we will look at finding and summing up numeric data types in a binary tree made of ‹std::tuple› instances. We will need a data type to stop the recursive data definitions: an empty tree, if you will. We will call it ‹null› (not to be confused with the C macro ‹NULL› nor with C++ ‹nullptr›. We want this to be a unique data type, but it does not need to carry any actual data, hence we can use an empty ‹struct›. struct null {}; /* C */ The summation will be defined recursively, so let's first define the overload for the base type: ‹null›. The neutral element of addition is 0, so let's use 0 as the value of an empty subtree. int sum( null ) { return 0; } /* C */ Now for the non-empty subtrees: we will use 3-tuples: the value in the node (integer) and the left and right subtrees. We will use template argument deduction to obtain the type of the composite tuple. Recall that we used to write function templates somewhat like this: template< typename T > int sum( const T &tup ) { int v = std::get< 0 >( tup ); // ... } This is not optimal, because there is a conflict with the ‹null› overload above: the template can be instantiated with ‹T = null›. The compiler will prefer the non-template version (or rather the most specific version), but the rules are complex and error-prone. It is better to not rely on those rules if they can be easily avoided. In this case, we can use a «more specific» (non-overlapping) overload, which will only accept 3-tuples. There is no chance that a ‹null› is confused for a 3-tuple. Nonetheless, we still need to figure out how to do template argument deduction in this case. Easier shown than described. We will use 2 template type parameters, for the left and right subtree. template< typename L, typename R > /* C */ However, we cannot directly use L and R as function arguments: we want to accept 3-tuples. Fortunately, the compiler can also deduce «parts» of an argument type: int sum( const std::tuple< int, L, R > &tup ) /* C */ { We can also use structured bindings to decompose the tuple, making the code easier to read: const auto &[ v, left, right ] = tup; /* C */ The rest of the function now looks like the most straightforward recursive definition from IB015. return v + sum( left ) + sum( right ); /* C */ } int main() /* demo */ /* C */ { std::tuple a{ 3, null(), null() }; std::tuple b{ 7, null(), null() }; std::tuple c{ 1, null(), null() }; std::tuple d{ 10, a, null() }; std::tuple e{ 2, b, c }; std::tuple f{ 0, d, e }; assert( sum( null() ) == 0 ); /* C */ assert( sum( a ) == 3 ); assert( sum( b ) == 7 ); assert( sum( c ) == 1 ); assert( sum( d ) == 13 ); assert( sum( e ) == 10 ); assert( sum( f ) == 23 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹method›] We already know that we can write class templates and function templates. It is only logical that we can also create method templates in C++ classes and in class templates. This example will be somewhat synthetic: we will have a class which does not permit direct access to its elements, but allows them to be folded using a function object. template< typename T > /* C */ class foldable { std::vector< T > data; public: A method to add elements to the internal container. void push( const T &t ) { data.push_back( t ); } /* C */ And the method template to accumulate the content using a function object. template< typename fun_t, typename init_t > /* C */ init_t fold( const fun_t &fun, init_t init ) { for ( const auto &e : data ) init = fun( init, e ); return init; } }; int main() /* demo */ /* C */ { foldable< int > f; f.push( 7 ); f.push( 3 ); assert( f.fold( std::plus<>(), 0 ) == 10 ); f.push( 10 ); assert( f.fold( std::multiplies<>(), 1 ) == 210 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹expr›] This example will demonstrate operator overloading in conjunction with class templates. Again, we will use argument deduction with partial types in function signatures, to match the desired types closely enough to to avoid ambiguities. You can probably imagine than an operator ‹+› that accept arbitrary types as arguments would not mesh very well with the rest of the program. First, we will define an ‹enum› to tag expressions with the operator they represent, and a ‹constant› class to use as leaf nodes. enum class expr_op { add, mul }; /* C */ Constants will be a straightforward class with an ‹eval› method, common with the ‹expr› class template below: struct constant /* C */ { int v; int eval() const { return v; } constant( int v ) : v( v ) {} }; We will start by defining an ‹expr› class template. template< typename left_t, typename right_t > /* C */ struct expr { expr_op op; left_t left; right_t right; expr( expr_op op, const left_t &l, const right_t &r ) /* C */ : op( op ), left( l ), right( r ) {} Compute the value of this node. int eval() const /* C */ { int l = left.eval(), r = right.eval(); switch ( op ) /* C */ { case expr_op::add: return l + r; case expr_op::mul: return l * r; } assert( false ); /* C */ } Like with normal operator overloading, there are multiple ways to overload operators for class templates. Let's start by defining a method. However, we immediately run into a problem: the right operand does not have to be of the same «type» as the left one, even though we want it to be an instance of the same class template. For that reason, we need to define the operator as a template method. template< typename l2_t, typename r2_t > /* C */ The return type is mildly infuriating, because it needs to spell out the composite instance. Next time, we will just use ‹auto›. expr< expr< left_t, right_t >, expr< l2_t, r2_t > > /* C */ Now for the signature of the operator itself: operator+( const expr< l2_t, r2_t > &o ) const /* C */ { Now that we have spelled out the monster types, the implementation is trivial. return { expr_op::add, *this, o }; /* C */ } Now let's try a ‹friend› definition. The gist is the same, and you may remember that we can still use ‹expr› without arguments to mean the instance with ‹left_t = left_t› and ‹right_t = right_t›. Then: template< typename l2_t, typename r2_t > /* C */ friend auto operator*( const expr &a, const expr< l2_t, r2_t > & b ) { But now we have a problem again. We are in the definition of a class template, and hence using the name of the class template without arguments means «this specific instance». But we want to construct a different instance, but using template argument deduction. We need to tell the compiler that is what we mean by using a qualified name for the class template: if qualified, the name no longer refers to this instance. return ::expr( expr_op::mul, a, b ); /* C */ } That covers the expression + expression cases. But we also need to be able to work with ‹constant› instances here. More operators! friend auto operator+( constant c, const expr &a ) /* C */ { return ::expr( expr_op::add, c, a ); } friend auto operator+( const expr &a, constant c ) /* C */ { return ::expr( expr_op::add, a, c ); } friend auto operator*( constant c, const expr &a ) /* C */ { return ::expr{ expr_op::mul, c, a }; } friend auto operator*( const expr &a, constant c ) /* C */ { return ::expr( expr_op::mul, a, c ); } }; That's not the end yet. We also need to be able to multiply and add two constants, to get a complete set. Since the result is an ‹expr› instance, it does not make much sense to put those into the ‹constant› class itself. Let's use toplevel definitions for those. Fortunately, in this case, the operators are not templates at least. auto operator+( constant a, constant b ) /* C */ { return expr( expr_op::add, a, b ); } auto operator*( constant a, constant b ) /* C */ { return expr( expr_op::mul, a, b ); } int main() /* demo */ /* C */ { constant a( 1 ); constant b( 2 ); auto c = a + b; assert( c.eval() == 3 ); assert( ( a * c ).eval() == 3 ); assert( ( a + c ).eval() == 4 ); assert( ( c + c ).eval() == 6 ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹set›] In which we will combine operator templates and template argument deduction to spice up the standard ‹std::set› container. We have already seen in ‹apply.cpp› that the compiler can deduce template arguments based on (self-contained) fragments of function argument types. We will use that along with operator templates to provide overloads for all instances of ‹std::set›, without affecting any other standard container, or any other type at all. We will overload operator ‹&› for intersection, operator ‹|› for union, ‹-› for standard difference and finally ‹^› for symmetric difference of two sets. Please keep in mind that the priorities of bitwise operators in C++ are unintuitive at best: overloaded operators inherit both their priority and associativity from the built-in ones. template< typename T > /* C */ std::set< T > operator&( const std::set< T > &a, const std::set< T > &b ) { std::set< T > out; Remember standard algorithms? std::set_intersection( a.begin(), a.end(), /* C */ b.begin(), b.end(), std::inserter( out, out.begin() ) ); return out; } Now the union. template< typename T > /* C */ std::set< T > operator|( const std::set< T > &a, const std::set< T > &b ) { std::set< T > out; std::set_union( a.begin(), a.end(), /* C */ b.begin(), b.end(), std::inserter( out, out.begin() ) ); return out; } And difference. This is getting a little boring. template< typename T > /* C */ std::set< T > operator-( const std::set< T > &a, const std::set< T > &b ) { std::set< T > out; std::set_difference( a.begin(), a.end(), /* C */ b.begin(), b.end(), std::inserter( out, out.begin() ) ); return out; } And finally the symmetric difference. Surprise! template< typename T > /* C */ std::set< T > operator^( const std::set< T > &a, const std::set< T > &b ) { return ( a | b ) - ( a & b ); } int main() /* demo */ /* C */ { std::set a{ 1, 2, 3 }; std::set b{ 1, 3, 5 }; std::set aib{ 1, 3 }; /* C */ std::set aub{ 1, 2, 3, 5 }; std::set amb{ 2 }; std::set axb{ 2, 5 }; assert( ( a & b ) == aib ); /* C */ assert( ( a | b ) == aub ); assert( ( a - b ) == amb ); assert( ( a ^ b ) == axb ); assert( ( a & b ) == ( b & a ) ); /* C */ assert( ( a | b ) == ( b | a ) ); assert( ( a - b ) != ( b - a ) ); assert( ( a ^ b ) == ( b ^ a ) ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹call›] The final example will deal with the function call operator, also known as ‹operator()›. This will allow us to construct objects which can be «called», just like lambdas. However, there is one thing that lambdas can't do very well in C++, and that is provide multiple «overloads». Likewise, standard top-level functions cannot be easily passed as arguments to other functions, since overload sets are not first-class in C++: instead, we have to wrap up the overload set in a callable object. We will see that in a minute. We will re-use the same construction that we have seen in ‹apply.cpp›, but we will allow different value types to appear in the tree, instead of just integers. We will again define ‹null› as the empty tree: struct null {}; /* C */ Even though the ‹struct› above is empty, we need to define equality if we want to use it. Since it will be rather useful in writing tests later, we will define the (trivial) equality operator here: bool operator==( null, null ) { return true; } /* C */ Like other functions defined on recursive data types, we first need to define the base case for ‹map›, i.e. the case when the subtree is empty: template< typename fun_t > /* C */ auto map( null, const fun_t & ) { return null(); } And instead of ‹sum›, we will have a generic mapping operator: template< typename val_t, typename left_t, typename right_t, /* C */ typename fun_t > auto map( const std::tuple< val_t, left_t, right_t > &tuple, const fun_t &fun ) { const auto &[ val, left, right ] = tuple; return std::tuple{ fun( val ), map( left, fun ), map( right, fun ) }; } The call operator uses syntax very similar to the indexing operator which we have seen before, just with parentheses instead of square brackets. Since ‹()› is the name of the operator, the arguments need to come in «another» pair of parens. Please keep that in mind! struct to_string /* C */ { std::string operator()( int i ) const { return std::to_string( i ); } std::string operator()( const std::string &s ) const /* C */ { return s; } }; A small thing that we have not seen yet. Normally, if we want to construct an object, we need to call its constructor, e.g. ‹std::string( "hello" )›. Sometimes, this is quite tedious, like in this example. C++ offers a feature called «user-defined literals», where we are allowed to overload certain operators which then make it possible to create object instances using «literal syntax». We will not get into the details of creating such user-defined literals, but we will use the one that the standard library provides for constructing ‹std::string› instances. To use them, we need to use the following declaration first, to get the literal operators into scope: using namespace std::literals; /* C */ int main() /* demo */ /* C */ { After the ‹using namespace› above, we can say "hello"s to mean ‹std::string( "hello" )›. Saves us a bit of typing. std::tuple a{ "hello"s, null(), null() }; /* C */ std::tuple b{ 7, null(), null() }; std::tuple c{ "x"s, a, b }; std::tuple b_str{ "7"s, null(), null() }; /* C */ std::tuple c_str{ "x"s, a, b_str }; assert( map( a, to_string() ) == a ); /* C */ assert( map( b, to_string() ) == b_str ); assert( map( c, to_string() ) == c_str ); } ## e. Elementary Exercises ### [‹format›] In this exercise, we will practice writing functions with more complex argument deduction. The functions in question will use ‹std::ostringstream› to produce string representation of sets and vectors. Use a comma-separated format for ‹std::vector› instances, with arbitrary element type, then do the same for ‹std::set›. Vectors should use square brackets ‹[]› and sets should use curly braces ‹{}› as delimiters. Assume the ‹value_type› stored in the vector has appropriate ‹std::ostream› operators. The functions should be called ‹format›. ### [‹concat›] Write a function which takes two sequences, ‹a› and ‹b›, and produces a single vector with values from the two sequences (first all values from ‹a›, then all values from ‹b›, preserving their order). Assume each sequence has a nested typedef ‹value_type›. The sequences do not need to be of the same type, but their values must be compatible. // … concat( … a, … b ) /* C */ ### [‹select›] Write a function which returns a vector of variants, such that the i-th position is taken from input ‹a› iff ‹which[ i ]› is true and from input ‹b› otherwise. Both ‹a› and ‹b› must have at least ‹which.size()› elements. Elements beyond this size are ignored. Both ‹a› and ‹b› are sequences with a ‹value_type› nested typedef. // … select( … a, … b, const std::vector< bool > &which ); /* C */ ## p. Preparatory Exercises ### [‹post›] The goal of this exercise is simple: take an oriented graph as the input and produce a list (vector) of vertices in the ‘leftmost’ DFS post-order. That is, visit the successors of a vertex in order, starting from leftmost (different exploration order will result in different post-orders). The graph is encoded as a neighbourhood list. template< typename value_t > /* C */ using graph = std::map< value_t, std::vector< value_t > >; Construct the post-order of ‹g› starting from vertex ‹i›. // … dfs_post( … g , … i ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹cons›] We will elaborate a little on the topic from ‹icons.cpp›, by making the type of ‹car› into a template argument. That way, we will be able to make a list that has items of different types in it. Generalize ‹cons› from the previous exercise and write a ‹reduce› function that takes an arbitrary ‹cons› instance, a function object (e.g. a lambda) and an initial accumulator value. The function object must be able to accept the accumulator as its first argument, and an arbitrary value that appears in the ‹cons› list as its second argument. null, cons, reduce callable object with overloads, for testing struct collect /* C */ { using pair = std::pair< int, double >; pair operator()( pair p, int i ) const /* C */ { p.first += i; return p; } pair operator()( pair p, double d ) const /* C */ { p.second += d; return p; } }; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹map›] Write ‹keyset›, a function which takes an instance of ‹std::map› and returns an ‹std::set› with the keys that were present in the input ‹map›. And ‹intersect›, which takes an ‹std::set› of keys and an ‹std::map› (with the same key type and arbitrary value type) and produces another ‹std::map› which only retains the key/value pairs for which keys were present in the input key set. ### [‹collect›] The goal of this exercise is to write a set of overloads that will, together, allow the user to extract values from standard containers, into a vector. For ‹std::map› and ‹std::unordered_map›, this means the value without the key. For other container types, the functions simply copy the contents into an output vector. ### [‹list›] In this exercise, we will define a ‹list› class that behaves like the lists in functional programming: the values and structure will be immutable, but it'll be possible to fairly cheaply create new lists by prepending values to existing lists. Define a class template ‹list› with a singe type parameter ‹T›, with the following interface (all methods are ‹const›): • ‹head› returns the value of the current list • ‹tail› returns the remainder of the list as another ‹list› instance, without copying any values • ‹empty› which returns ‹true› if the list is empty • a default constructor which creates an empty list (i.e. the ‹[]› constructor you know from Haskell) • a 2-parameter constructor which takes the value and the tail of the list (i.e. the ‹(:)› constructor). Hint: It is preferable to store the values «inline» in the nodes. You should also use 2 data types, one for the list itself and another for nodes: this will make it easier to implement empty lists and in general make the implementation nicer. template< typename T > /* C */ class list; /* ref: 24 lines */ ### [‹tree›] In this exercise, we will implement an immutable binary tree, similar to the list we saw earlier. Implement class template ‹tree› with a single type argument ‹T› and the following interface (all method are ‹const›): • the default constructor creates an empty tree, • a 3-parameter constructor creates a tree with the value given in the first argument as root and the next two arguments specify the left and right subtrees, • a 1-parameter constructor creates a leaf node which stores the value given in the argument, • ‹empty› returns true if the tree is empty, • ‹root› returns a reference to the value in the root, • ‹left› returns the left subtree, • ‹right› returns the right subtree. Hint: two of the constructors can be merged using default arguments. template< typename T > /* C */ class tree; ## r. Regular Exercises ### [‹icons›] In this exercise, your goal will be to define a list-like structure using templates, and then recursively sum numbers stored in it. You will need to use both templates and function overloading. In LISP-like languages, lists are built out of so-called «cons cells» (cons being short for constructor). Each cell contains a value and a pointer to the next cell. The value is traditionally called ‹car› and the pointer to the next cell is called ‹cdr›. The ‹cdr› may also be ‹null›, i.e. an empty list. In our case, the ‹car› will always be of type ‹int›. The ‹cdr› will be given by a template parameter, in the expectation that it is another ‹cons› instance or ‹null›. struct null {}; /* empty */ /* C */ template< typename cdr_t > /* C */ struct cons; Overloads and/or templates of ‹sum› go here. ### [‹sorted›] Write a function object that will decide, when passed to ‹foreach›, whether the iterated sequence was sorted in ascending order. The result is obtained by calling ‹was_sorted› on the object after the iteration ends. It might be useful to know that ‹std::any› can hold a value of any type. Use normal assignment to store a value in an ‹any› instance and ‹std::any_cast› to read the value back. struct check_sorted; /* C */ Unlike ‹std::foreach›, we take the function by reference, which makes it possible to inspect its state after iteration ends. template< typename iter_t, typename fun_t > /* C */ void foreach( iter_t b, iter_t e, fun_t &&f ) { while ( b != e ) f( *b++ ); } ### [‹fsm›] Everyone's favourite: deterministic finite state machines. We will write a class template that will let us decide whether a generalized word (a sequence of values of a type equipped with equality) belongs to a regular language described by a given finite automaton (finite state machine) or not. The type which represents individual letters is given to ‹fsm› as a type parameter. The constructor of ‹fsm› should accept a single boolean (mark the constructor ‹explicit›), which determines if the state represented by the instance is accepting or not. A default-constructed ‹fsm› should be non-accepting. Make the following methods available: • ‹next› which accepts a letter (of type ‹letter_t›) and a ‹const› reference to another ‹fsm› instance; the method adds a transition to the automaton, • ‹accept› which takes a sequence of ‹letter_t› values (the type of the sequence is not fixed, but it can be iterated using a range ‹for›) and returns ‹true› if the automaton accepts the word stored in the sequence; this method should be marked ‹const›. // … class fsm; /* C */ ### [‹bimap›] Implement ‹bimap›, a container with 2 key types (say ‹left_t› and ‹right_t›) and the following interface: • ‹insert( left_t, right_t )› establish a mapping and return ‹true›, or return ‹false› if either of the passed keys is already mapped (to the same or a different value), • ‹get_left( right_t )›: return a pointer to the left key corresponding to a given right key or ‹nullptr› if not present, • ‹get_right( left_t )›: the opposite, • ‹erase( left_t, right_t )› remove a mapping. ### [‹tinyvec›] † Implement ‹tiny_vector›, a class which works like a vector, but instead of allocating memory dynamically, it uses a fixed-size buffer which is part of the object itself (use e.g. an ‹std::array› of bytes). The number of elements it should be able to accommodate is given as a template parameter, along with the type of the element. Provide the following methods: • ‹insert› (take an index and an rvalue reference), • ‹erase› (take an index), • ‹back› and ‹front›, with appropriate return types. In this exercise (unlike in most others), you are allowed to use ‹reinterpret_cast›. Throw this if ‹insert› is attempted but the element wouldn't fit into the buffer. class insufficient_space {}; /* C */ Hint: Use ‹uninitialized_*› and ‹destroy(_at)› functions from the ‹memory› header. template< typename T, size_t max_size > /* C */ class tiny_vector; void do_test(); /* C */ # Iterators XXX Demonstrations: 1. ‹queue› – an iterable queue 2. ‹split› – chop up a ‹string_view› into pieces 3. ‹glob› – iterate over matches of a pattern in a string Elementary exercises: 1. ‹iota› – an iterable sequence of integers 2. ‹view› – iterate a slice of an existing collection 3. ‹skip› – iterate every n-th item (a stride) of a collection Preparatory exercises: 1. ‹seq› – generic sequences 2. ‹filter› – filtered sequences 3. ‹zip› – iterate two sequences in lockstep 4. ‹nibble› – a fixed-size nibble array 5. ‹tree› – in-order iteration of a tree 6. ‹scan› – generalized prefix sum Regular exercises: 1. ‹map› – applying a function to a sequence 2. ‹range› – views with a shared backing store 3. ‹permute› – iterate all permutations of a sequence 4. ‹critbit› – iterate a ‘critbit’ binary trie in order 5. ‹matrix› † – iterate a compact rectangular array 6. ‹bits› – iterate bits in a word ## d. Demonstrations ### [‹queue›] We will now implement a data structure ‘from scratch’ (i.e. without using ‹std› containers) using templates. Again, you may find it useful to go back to ‹06/queue.cpp› and compare the two implementations. Like before, since we are going for a custom, node-based structure, we will need to first define the class to represent the nodes. Unlike the previous implementation, however, the node itself needs to be parametrized by the type of the value it should hold. template< typename T > /* C */ struct queue_node { You may have noticed this with the ‹zipper› earlier: we do not need to mention the type parameter when we want to refer to the instance of ‹queue_node› where ‹T› is ‹T› (though we can if we want to). In other words, within ‹queue_node›, saying ‹queue_node› when referring to a type means the same thing as ‹queue_node< T >›. std::unique_ptr< queue_node > next; /* C */ The data attribute will be of type ‹T›. T value; /* C */ }; Like the node, the iterator also needs to be parametric. template< typename T > /* C */ struct queue_iterator { Here, we have no choice but to explicitly spell out the type parameter of ‹queue_node›, since we are no longer within that class. queue_node< T > *node; /* C */ Constructor names are unaffected by templates. queue_iterator( queue_node< T > *n ) : node( n ) {} /* C */ The pre-increment operator simply shifts the pointer to the ‹next› pointer of the currently active node. This method is unchanged from the non-generic version. queue_iterator &operator++() /* C */ { node = node->next.get(); return *this; } The implicit ‘current instance of the template’ shortcut works in arguments too, including in arguments of ‹friend› functions, so let's demonstrate that: friend bool operator!=( const queue_iterator &a, /* C */ const queue_iterator &b ) { return a.node != b.node; } Finally the dereference operator. Unlike before, we don't know much about ‹T›, hence we prefer to always return a reference, even in the ‹const› overload. T &operator*() { return node->value; } /* C */ const T &operator*() const { return node->value; } }; The ‹queue› itself will be a template too, of course. template< typename T > /* C */ class queue { Like in the iterator, we need to instantiate any template classes that we use that were defined earlier. That is the only difference compared to our earlier ‹queue› implementation. std::unique_ptr< queue_node< T > > first; /* C */ queue_node< T > *last = nullptr; public: In the integer-only version, we passed the argument by value, but like in the dereference operator above, we will now instead use a ‹const› reference: ‹T› might be a big class with an expensive copy operation. We do not want to do that twice. void push( const T &v ) /* C */ { if ( last ) /* non-empty list */ { Notice the ‹T› in the ‹make_unique› call. last->next = std::make_unique< queue_node< T > >(); /* C */ last = last->next.get(); } else /* empty list */ { first = std::make_unique< queue_node< T > >(); last = first.get(); } last->value = v; /* C */ } Now we run into a bit of a problem. Since making copies of ‹T› is possibly expensive, we would like to return a reference: but we cannot, since ‹pop› will destroy the node which stores the value. Incidentally, this is the reason why ‹std::queue::pop› is a void function and you need to use a separate ‹front› call to get the value. We will simply return by value instead, which can be less efficient, but not terribly so. We can reduce the cost by using ‹std::move› on the value, since the node is going to be destroyed anyway. T pop() /* C */ { T v = std::move( first->value ); first = std::move( first->next ); Do not forget to update the ‹last› pointer in case we popped the last item. if ( !first ) last = nullptr; /* C */ return v; } The emptiness check is simple enough. bool empty() const { return !last; } /* C */ Same as before, but we need to instantiate the ‹queue_iterator› template. queue_iterator< T > begin() { return { first.get() }; } /* C */ queue_iterator< T > end() { return { nullptr }; } Same. void erase_after( queue_iterator< T > i ) /* C */ { assert( i.node->next ); i.node->next = std::move( i.node->next->next ); } }; int main() /* demo */ /* C */ { We start by constructing an (empty) queue and doing some basic operations on it. We start by inserting and removing a single element. queue< std::pair< long, long > > q; /* C */ assert( q.empty() ); /* C */ q.push( { 7, 0 } ); assert( !q.empty() ); assert( q.pop() == std::pair( 7l, 0l ) ); assert( q.empty() ); Now that we have emptied the queue again, we add a few more items and try erasing one and iterating over the rest. q.push( { 1, 0 } ); /* C */ q.push( { 2, 0 } ); q.push( { 7, 0 } ); q.push( { 3, 0 } ); We check that erase works as expected. queue_iterator i = q.begin(); /* C */ ++ i; assert( *i == std::pair( 2l, 0l ) ); q.erase_after( i ); We can still use instances of ‹queue› in range ‹for› loops, because they have ‹begin› and ‹end›, and the types those methods return (i.e. iterators) have dereference, inequality and pre-increment. Since our current instance of ‹queue› contains pairs, we can also use structured bindings in the ‹for› loop. int x = 1; /* C */ for ( auto [ v, w ] : q ) /* C */ { assert( v == x++ ); assert( w == 0 ); } That went rather well, let's just check that the order of removal is the same as the order of insertion (first in, first out). This is how queues should behave. assert( q.pop() == std::pair( 1l, 0l ) ); /* C */ assert( q.pop() == std::pair( 2l, 0l ) ); assert( q.pop() == std::pair( 3l, 0l ) ); assert( q.empty() ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹split›] We will forego templates for a bit and implement a full-featured «input iterator»: the most basic kind of iterator that can be used to obtain values (as opposed to updating them, as done by an «output iterator»). A common task is to split a string into words, lines or other delimiter-separated items. This is one of the cases, where the standard library does not offer any good solutions: hence, we will roll our own. The class will be called ‹splitter› and will take 2 parameters: the string (‹string_view› to be exact) to be split, and the delimiter (for simplicity limited to a single character). The splitter is based on ‹string_view› to make the whole affair ‘zero-copy’: the string data is never copied. The downside is that the input string (the one being split) must ‘outlive’ the ‹splitter› instance. You might remember the function ‹split› from week 9: we will use that as the ‘workhorse’ of the ‹splitter›. using split_view = std::pair< std::string_view, std::string_view >; /* C */ split_view split( std::string_view s, char delim ) /* C */ { size_t idx = s.find( delim ); if ( idx == s.npos ) return { s, {} }; else return { s.substr( 0, idx ), s.substr( idx + 1, s.npos ) }; } The ‹splitter› class itself doesn't do much: its main role is to create iterators, via ‹begin› and ‹end›. To this end, it must of course remember the input string and the delimiter. struct splitter /* C */ { using value_type = std::string_view; std::string_view _str; char _delim; Real iterators «must» provide ‹operator->› – the one that is invoked when we say ‹iter->foo()›. For this particular use-case, this is a little vexing: ‹operator->› «must» return either a raw pointer, or an instance of a class with overloaded ‹operator->›. Clearly, that chain must end somewhere – sooner or later, we must have an «address» of the item to which the iterator points. This is inconvenient, because we want to construct that item ‘on the fly’ – whenever it is needed – and return it from the dereference operator (unary ‹*›) «by value», not by reference. The above two requirements are clearly contradictory: as you surely know, returning the address of a local variable won't do. This is where ‹proxy› comes into play: its role is to hold a copy of the item that has sufficiently long lifetime. Later, we will return ‹proxy› instances by value, hence ‹proxy› itself will be a temporary object. Since temporary objects live until the ‘end of statement’ (i.e. until the nearest semicolon, give or take), we can return the address of its own attribute. That address will be good until the entire statement finishes executing, which is good enough: that means when we write ‹iter->foo()›, the proxy constructed by ‹operator->›, and hence the ‹string_view› stored in its attribute, will still exist when ‹foo› gets executed. struct proxy /* C */ { value_type v; value_type *operator->() { return &v; } }; struct iterator /* C */ { There are 5 ‘nested types’ that iterators must provide. The probably most important is ‹value_type›, which is the type of the element that we get when we dereference the iterator. using value_type = splitter::value_type; /* C */ The ‹iterator_category› type describes what kind of iterator this is, so that generic algorithms can take advantage of the extra guarantees that some iterators provide. This is a humble «input iterator», and hence gets ‹std::input_iterator_tag› as its category. using iterator_category = std::input_iterator_tag; /* C */ The remaining 3 types exist to make writing generic algorithms somewhat easier. The ‹difference_type› is what you would get by subtracting two iterators – the ‘default’ is ‹ssize_t›, so we use that, even though our iterators cannot be subtracted. using difference_type = ssize_t; /* C */ The last two are ‘decorated’ versions of ‹value_type›: a pointer, which is straightforward (but do remember to take ‹const›-ness into account)… using pointer = value_type *; /* C */ … and a reference (same ‹const› caveat applies). However, you might find it surprising that the latter is not actually a reference type in this case. Why? Because ‹reference› is defined as ‘what the dereference operator returns’, and our ‹operator*› returns a value, not a reference. Input iterators have an exception here: all higher iterator types (forward, bidirectional and random) must make ‹reference› an actual reference type. using reference = value_type; /* C */ Now, finally, for the implementation. The data members (and the constructor, and the assignment operator) are all straightforward. The ‹_str› attribute represents the reminder of the string that still needs to be split, and will be an empty string for the ‹end› iterator. Remember that ‹string_view› does not hold any data, so we are «not» making copies of the input string. std::string_view _str; /* C */ char _delim; iterator( std::string_view s, char d ) /* C */ : _str( s ), _delim( d ) {} iterator &operator=( const iterator & ) = default; /* C */ The pre-increment and post-increment operators are reasonably simple. As is usual, we implement the latter in terms of the former. iterator &operator++() /* C */ { _str = split( _str, _delim ).second; return *this; } iterator operator++( int ) /* C */ { auto orig = *this; ++*this; return orig; } Dereference would be unremarkable, except for the part where we return a value instead of a reference (we could use the ‹reference› nested type here to make it clear we are adhering to iterator requirements, but that would be likely more confusing, considering how ‹reference› is not a reference). Do note the ‹const› here. value_type operator*() const /* C */ { return split( _str, _delim ).first; } This is what gets called when we write ‹iter->foo›. See ‹proxy› above for a detailed explanation of how and why this works. Also, ‹const› again. proxy operator->() const /* C */ { return { **this }; } Finally, equality. There is a trap or two that we need to avoid: first and foremost, ‹string_view› comparison operators compare «content» (i.e. the actual strings) – this is not what we want, since it could get really slow, even though it would ‘work’. The other possible trap is that on many implementations, string literals with equal content get equal addresses, i.e. the ‹begin› of two different ‹std::string_view( "" )› instances would compare equal, but this is «not» guaranteed by the standard. It just happens to work by accident on many systems. bool operator==( const iterator &o ) const /* C */ { return _str.empty() && o._str.empty() || _str.begin() == o._str.begin(); } bool operator!=( const iterator &o ) const /* C */ { return !( *this == o ); } }; auto begin() const { return iterator( _str, _delim ); } /* C */ auto end() const { return iterator( {}, _delim ); } splitter( std::string_view str, char delim ) /* C */ : _str( str ), _delim( delim ) {} }; int main() /* demo */ /* C */ { auto s = splitter( "quick brown fox", ' ' ); auto e = std::vector{ "quick", "brown", "fox" }; auto iseq = [&]{ return std::equal( s.begin(), s.end(), /* C */ e.begin(), e.end() ); }; assert( iseq() ); /* C */ s = splitter( "", ' ' ); /* C */ assert( !iseq() ); e.clear(); assert( iseq() ); s = splitter( "hello", ' ' ); /* C */ e = std::vector{ "hello" }; assert( iseq() ); s = splitter( "hello", 'l' ); /* C */ e = std::vector{ "he", "", "o" }; assert( iseq() ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹glob›] TBD ## e. Elementary Exercises ### [‹iota›] Write a class ‹iota›, which can be iterated using a ‹range› for to yield a sequence of numbers in the range ‹start›, ‹end - 1› passed to the constructor. class iota; /* C */ ### [‹view›] Write a class template ‹view› which will allow us to bundle a pair of iterators of an existing sequence and use them as a virtual sequence in its own right. It should be possible to change the underlying sequence through the view. The constructor should accept two iterators, ‹start› and ‹end›, and iterating the resulting sequence should include everything in this range (‹end› is excluded, as is customary). // … class view; /* C */ ### [‹skip›] Write a class template ‹skip› which will be like a view, but allow us to iterate every n-th item (a stride) of a given iterator range, instead of every item. Make sure that if the stride does not evenly divide the sequence length, iteration still works correctly. Hint: please note that this class is significantly more complicated than ‹view› was. You might find ‹decltype( auto )› useful, particularly as the return type of a function. ## p. Preparatory Exercises ### [‹seq›] In this exercise, the goal will be to implement a class template which will allow us to iterate over a sequence of number-like objects. The ‹seq› class should be constructible from 2 number-like objects: the initial value (included) and the final value (excluded). Use pre-increment (operator ‹++›) and equality (operator ‹==›) to generate the values. The dereference operator should return the generated objects by value. template< typename T > /* C */ class seq_iterator; /* ref: 10 lines */ template< typename T > /* C */ class seq; /* ref: 9 lines */ A ‹nat› implementation for testing purposes. struct nat /* C */ { int v; nat( int v ) : v( v ) {} bool operator==( nat o ) const { return v == o.v; } nat &operator++() { ++v; return *this; } }; ### [‹filter›] Lazy sequences, part two. Define a class template, ‹filter›, which holds two items: • a «reference» to an arbitrary container, • a lambda ‹f› (of type ‹a → bool›). The constructor of ‹filter› should accept both, in this order. It should be possible to use instances of ‹filter› in range ‹for› loops: each element from the underlying container is first passed to ‹f› and if the result is true, is returned to the user (via the dereference operator), otherwise it is discarded. You may want to review ‹map.cpp› in this unit and ‹filter.cpp› from week 7. template< typename, typename > /* C */ struct filter_iterator; /* ref: 25 lines */ template< typename, typename > /* C */ struct filter; /* ref: 11 lines */ ### [‹zip›] Lazy sequences, part three. Define a class template, ‹zip›, which holds 2 references to arbitrary containers, possibly of different types. The constructor of ‹zip› should accept both (via ‹const› references). It should be possible to use instances of ‹zip› in range ‹for› loops: in each iteration, the ‹zip› iterator fetches a single elemnt from each of the two containers and returns them as a 2-tuple of ‹const› references. The iteration ends when the shorter of the two sequences runs out of elements. Hint: to create a tuple of references, use ‹std::tie›. template< typename, typename > /* C */ struct zip_iterator; /* ref: 31 lines */ template< typename, typename > /* C */ struct zip; /* ref: 11 lines */ ### [‹nibble›] In this exercise, we will create a fixed-size array of nibbles (half-bytes), with an indexing operator and with a basic iterator. You may want to refer back to ‹05/nibble.cpp› for details about operators. The class template ‹nibble_array› should take a single ‹size_t›-typed non-type template argument. It should be possible to index the array and to iterate it using a range ‹for› loop. The storage size should be the least required number of bytes. Default-constructed ‹nibble_array› should have zeroes in all its entries. template< size_t N > /* C */ class nibble_array; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹tree›] Write an iterator class and 2 functions, ‹tree_begin› and ‹tree_end›, which given a proper binary tree (by const reference) construct an iterable range which visits each node of the tree once. The iteration should proceed in-order, that is, the entire left subtree is visited before the current node, and the right subtree afterwards. template< typename value_t > /* C */ struct tree { std::unique_ptr< tree > left, right; tree *parent = nullptr; value_t value; static auto make_tree( const tree &t, tree *parent ) /* C */ { return std::make_unique< tree >( t, parent ); } tree( const tree &t, tree *parent ) /* C */ : left( t.left ? make_tree( *t.left, this ) : nullptr ), right( t.right ? make_tree( *t.right, this ) : nullptr ), parent( parent ), value( t.value ) {} tree( value_t value, const tree &l, const tree &r ) /* C */ : left( make_tree( l, this ) ), right( make_tree( r, this ) ), value( std::move( value ) ) {} tree( value_t value ) /* C */ : value( std::move( value ) ) {} }; Given a tree ‹t›, construct the two end-points of the iterator range: // … tree_begin( … t ); /* C */ // … tree_end( … t ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹scan›] Implement a class template ‹scan›, which computes a «generalized prefix sum». This is essentially a fold, but instead of simply returning the final value, it also returns all the intermediate results. It should be possible to iterate instances of ‹scan› using a range ‹for› loop. Example: a scan of a sequence with elements 1, 2, 3 and 4, using ‹std::plus›, and initial value 0 should yield the sequence 1, 3, 6, 10. The constructor, which should enable class template argument deduction, takes 3 arguments: • a ‹const› reference to a container with ‹value_type = T›, • a lambda with the signature ‹S( T, S )›, and • an initial value of type ‹S›, by value. NB. Do not assume that values of either type ‹S› or ‹T› can be copied. Values of type ‹S› can be default-constructed though. ## r. Regular Exercises ### [‹map›] Lazy sequences, part one. Define a class template, ‹map›, which holds two items: • a reference to an arbitrary container, • a lambda ‹f› (of type ‹a → b›). The constructor of ‹map› should accept both (via ‹const› references), in this order. It should be possible to use instances of ‹map› in range ‹for› loops: each element from the underlying container is first passed to ‹f› and the result of that is returned to the user (via the dereference operator). Hint: the type of the iterator that ‹const› versions of ‹begin› and ‹end› return is available in standard containers as a «nested type», like this: ‹std::vector< int >::const_iterator›. ### [‹range›] We have mostly ignored the question of ownership when we worked with on-the-fly ‹map› and ‹filter› implementations in previous exercises: it was up to the user to ensure that the underlying container outlives the range object. However, we may sometimes want to be able to return such mapped ranges from functions which construct the underlying data, and this does not work in our previous model. Let's try a different approach then. Define a class template ‹range› which takes a «container» as a template argument. The class should offer the following interface (all methods are ‹const›): • construction from a container, with argument template deduction (we will make a copy of the container: in real world, we would be able to avoid doing that), • iteration interface: ‹begin› and ‹end› which return suitable iterators (usable in range ‹for› at minimum), • ‹take› and ‹drop› which construct a new ‹range› object that shares the backing data with the current one, but offer a reduced view of it (‹take› reduces the view to first N elements, while ‹drop› removes the first N elements from the view), • an «element-wise» equality comparison operator (we want this to work with ranges backed by different containers, so you will need to implement this operator as a template). template< typename container_t > /* C */ class range; ### [‹permute›] Implement class ‹permutations›, with a constructor which accepts an ‹std::vector› of ‹int› and which represents a sequence of all distinct permutations of the input vector. Iterating an instance of ‹permutations› should yield values which can be both iterated and indexed, yielding, in turn, integers. The ‹permutations› object itself does not need to be indexable. For example: std::vector vec{ 1, 3, 2 }; for ( auto p : permutations( vec ) ) for ( int v : p ) std::cerr << v << " "; The first permutation should be sorted in ascending order. The «sequence of permutations» as a whole should be sorted in lexicographic order (as a consequence of this, the last permutation should be sorted in descending order). The output of the above program, therefore, should be: 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1. ### [‹matrix›] † Implement a class which stores a compact N×M array of scalars (given by its template parameter). Assume that the scalars can be copied and default-constructed, but avoid constructing unnecessary copies. The data is passed into the constructor as a single ‹std::initializer_list< scalar_t >› with N×M entries, in row order, starting in the top left corner. The class should have 2 methods: ‹cols› and ‹rows›. The result of each should be iterable, and each «element» yielded should be iterable again, yielding the corresponding scalars. template< typename scalar_t, size_t ncols, size_t nrows > /* C */ class matrix; void do_test(); /* C */ ### [‹bits›] Implement a class template ‹bits› that, when iterated, yields individual bits of the number passed into its constructor, as boolean values. template< typename number_t > /* C */ class bits; # Review Since this is a review chapter with no new material in it, there are no demonstrations. You can refer to previous chapters and lectures if you encounter concepts that you do not know, or which you need to refresh. Elementary exercises: 1. ‹digraph› – count digraph frequency 2. ‹spelling› – a very simple spell checker 3. ‹ternary› – ternary logic Preparatory exercises: 1. ‹chords› – naming minor and major 5 & dominant 7 chords 2. ‹grammar› – generate words from regular grammars 3. ‹linear› – simple linear equations with a parser 4. ‹poly› – searching for roots of polynomials 5. ‹queens› – checking a solution of the 8 queens puzzle 6. ‹map› – more mapping of sequences Regular exercises: 1. ‹trie› – binary tries with wildcards 2. ‹cooking› – storing recipes 3. ‹cards› – parsing and comparing playing cards 4. ‹minilisp› – a small LISP-like language parser 5. ‹language› – detect language of a text fragment 6. ‹union› – the union-set data structure ## e. Elementary Exercises ### [‹digraph›] We will write a simple function, ‹digraph_freq›, which accepts a string and computes the frequency of all (alphabetic) digraphs. The exact signature is up to you, in particular the return type. The only requirement is that the returned value can be indexed using strings and this returns the count (or 0 if the input string is not a correct digraph). This must also work on ‹const› instances of the return value. For examples see ‹main›. Define ‹digraph_freq› here, along with any helper functions or classes. ### [‹spelling›] The file ‹/usr/share/dict/words› contains one English word per line. Write a class, ‹spell›, the constructor of which takes the path to a file of this type (one word per line) and with a single method, ‹check›, which takes an ‹std::string› which contains a single word and returns ‹true› if the word is in the provided list. class spell; /* C */ ### [‹ternary›] Ternary (or 3-valued) logic uses 3 different truth values: true, false and unknown (maybe). Define a suitable type ‹tristate› and 3 constants ‹yes›, ‹no› and ‹maybe› (to avoid conflicts with built-in boolean constants), along with the standard logical operators and equality. ## p. Preparatory Exercises ### [‹chords›] We will write a simple program to compute and format chords (as in music). Partition the code as you see fit. The entire western musical scale has 12 notes in it, one semitone (100 cents) apart. Chords are built up from minor (300 cents) and major (400 cents) thirds. We will only deal with chords in the root position, i.e. where the root note is in the bass and we'll use the German names: • c, d, e, f, g, a and h are the ‘base’ notes • cis, dis, eis = f, fis, gis, ais, his = c are sharps, • ces = h, des, es, fes = e, ges, as and b are flats. Base notes are 200 cents apart, except the e/f and h/c pairs, which are 100 cents apart. A flat subtracts, and a sharp adds, 100 cents to the base note. The simplified rules for using note names in chords are as follows: • key is E, G, A, H, D or any note with a sharp → use sharps, • key is F or a note with a flat → use flats, • flats and sharps are not mixed in basic chords, • ignore double flats and double sharps • instead of eis, his, ces and fes, use f, c, h and e. A (pure) fifth is 700 cents and a minor 7th is 1000 cents. Intervals (in cents) are composed using addition, mod 1200. By convention, ‘c’ is 0. For instance, if the root is ‘g’, that is 700 cents, adding a pure fifth yields 1400 mod 1200 = 200 = ‘d’. Notes ‘g’ and ‘d’ are a fifth apart. The major fifth chord starts at the key note (tonic) + a major third + minor third, e.g. ‘c’ → ‘c e g’ or ‘e’ → ‘e gis h’. std::string major_5( std::string key ); /* C */ The root of a minor fifth chord is a sixth above the key note and adds a minor third + a major third, e.g. ‘c‘ → ‘a c e’ or ‘e’ → ‘cis e gis’. Alternatively, you could think of it as a minor third «below» the key note, the key note itself, and a major third above. std::string minor_5( std::string key ); /* C */ The root of a dominant 7th chord is a fifth above the key note (tonic): for key ‘c’, the root of the dominant is ‘g’. On top of the root, add a major third, a minor third, and another minor third. E.g. ‘f’ → ‘c e g b’ std::string dominant_7( std::string key ); /* C */ ref: 42 lines in 4 helper functions, major_5, minor_5 & dominant_7 are each 1 line ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹grammar›] A regular grammar has rules of the form ⟦A → xB⟧ or ⟦A → x⟧ where ⟦A⟧ and ⟦B⟧ are non-terminals and ⟦x⟧ is a terminal. Implement class ‹grammar› which is default-constructible and has 2 methods: • ‹add_rule›, which takes 2 or 3 arguments: a single ‹char› for the left-hand non-terminal (a capital alphabetic letter), a terminal and optionally another non-terminal, • ‹generate›, a ‹const› method which takes 2 arguments: the starting non-terminal and an integer which gives the maximum length of a word, and returns an ‹std::vector› of ‹std::string› with all the «words» the grammar can generate (within the given size bound), sorted lexicographically. class grammar; /* C */ ### [‹linear›] Remember the linear equation solver from ‹06/p6_linear.cpp›? Let's do that again, but this time with a simple parser instead of operator overloading. Write a function ‹solve› which takes a string as its input, and returns an ‹std::pair› of floating point numbers. The input contains 2 linear equations, one per line, with 2 single-letter alphabetic variables and integer coefficients. The result should be ordered alphabetically (e.g. x, y). std::pair< double, double > solve( const std::string &eq ); /* C */ ref: solve 26 lines, helper class 19 lines ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹poly›] In this exercise, we will find at least one (real) root of an odd-degree polynomial (this is guaranteed to exist, and is comparatively easy to find using the intermediate value theorem and binary search). Write function ‹find_root› which takes 2 arguments: • a vector of coefficients (each represented as a ‹double›), sorted from the highest-degree to the lowest, e.g. ⟦x³ - 3x + 2⟧ is represented as the vector ‹{ 1, 0, -3, 2 }›, • an ‹std::pair› with a lower and an upper bound on the value of the root. The function should return a root ⟦x⟧, that is, a number for which the polynomial evaluates to zero, e.g. ⟦x = -2⟧ for the above example. The pre-condition is that the bounds evaluate to numbers with opposite signs (and hence the interval must contain at least one root). There might be multiple roots in the interval, though: it does not matter which one the function finds. The returned ‹double› should be within `1e-5` of the actual value of the root. using bounds = std::pair< double, double >; /* C */ using poly = std::vector< double >; double find_root( const poly &, bounds ); /* ref: 28 lines */ /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹queens›] Write a function that checks whether a given configuration of 8 queens on a chessboard is such that no two queens endanger each other. The first number is the column numbered from left, a = 1, b = 2, ...; second is the row (likewise indexed from 1, starting at bottom): { 1, 1 } is the bottom left corner. using position = std::pair< int, int >; /* C */ using queens = std::vector< position >; Return true if all queens are safe. bool check( const queens &q ); /* ref: 43 lines */ /* C */ ### [‹map›] We will revisit one of the sequence-related constructs from earlier, that of on-the-fly (on demand) transformation (mapping) of elements using a given function. In particular, we will look at composing maps. In this exercise, you should implement a ‹map› view like the one we did in ‹11/r1_map›, with one improvement: it should also work with functions which return references. The easiest way to do that is by creating a type alias using ‹decltype› and use that as the return type of the dereference operator. (Alternatively, look up ‹decltype( auto )› in cppreference, though that wouldn't work if you wanted to use the type as a component of another type.) ## r. Regular Exercises ### [‹trie›] We will implement a binary trie (see ‹07/p2_bittrie.cpp› for more details about the data structure) with a twist. The user will manage the nodes explicitly, for two reasons: doing it automatically is a fair amount of work, and we want to be able to share subtrees. In particular, our present trie implementation will be able to encode 0 and 1 bits in a key, but also a ‹?›: a bit which will not affect the outcome. The easiest way to achieve this is by pointing both the left and the right pointer of a tree to the same child node. To make things even more interesting, each leaf node should be able to reconstruct its own key, with question marks always taken to be ones. The interface: • ‹root› returns a suitable pointer to the root node, • ‹add› takes a suitable node pointer (the parent node), and the bit to append (a ‹bool›), • ‹add_amb› takes a node and appends a question mark to it, • ‹find› takes an ‹std::vector› of ‹bool› and returns a shared pointer to the corresponding node (or a ‹nullptr› if not found). The default-constructed ‹trie› should be empty. Both ‹add› and ‹add_amb› should return a (shared) pointer to the new node. The nodes should provide the method ‹key› which returns an ‹std::vector› of ‹bool›. The nodes must not store the entire key. class trie_node; /* ref: 26 lines */ /* C */ class trie; /* ref: 30 lines */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹cooking›] In this exercise, we will implement a simple model of cooking, with recipes and a pantry. Try to think about code duplication and whether you can reduce it and what is the cost of reduction in duplication. The class ‹pantry› will keep a list of available ingredients and their quantity. It should be default-constructible and offer a method ‹add›, which takes a string (the name of the ingredient) and an integer (the quantity). A ‹const› method ‹count› should take a string (name of the ingredient) and return the quantity available (possibly 0). class pantry; /* C */ We will use another class to represent recipes (in our simplified world, a list of ingredients and quantities required to cook a meal). Like ‹pantry›, it should be default-constructible and offer a method ‹add›, which accepts 2 or 3 arguments: name, the «required» quantity and, if supplied, an «optional» quantity of the ingredient that will be used if available («in addition to» the required amount) but is not required to cook the meal. class recipe; /* C */ Finally, implement function ‹cook› with 3 arguments: a «mutable» reference to the ‹pantry› which will be used to obtain the ingredients, a ‹const› reference to the ‹recipe› to cook and an ‹int›, the number of portions to prepare. The function then returns ‹true› if everything went okay (and of course deducts the ingredients used up from the ‹pantry›) or ‹false› if some ingredient was missing or there wasn't enough of it, in which case the ‹pantry› content remains unchanged. bool cook( pantry &, const recipe &, int qty ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹cards›] In this exercise, we will look back at input/output streams and formatting operators. Implement class ‹card› which represents one card from the standard 52-card deck, along with operators for input and output. The format is two letters, first the rank and then the suit. The rank 10 is represented as ‹T›. Use S, H, C and D to represent suits. Do not forget to handle errors. class card; /* C */ # T.3. Tasks with Templates and Iterators The programming tasks for this block are as follows: 1. ‹tree.*› – iterating trees in post-order, 2. ‹bplus.*› – a class template implementing B+ trees, 3. ‹linalg.*› – complex and real linear algebra, 4. ‹lisp.*› – a simple interpreter for a LISP-like language. All tasks in this block make some use of templates, which are covered in chapters 9 and 10, and of course they also rely on knowledge from previous blocks. Since the first task is about writing iterators, you will also need to understand the material from chapter 11 to complete it (though you should be able to make considerable progress with what you know from chapter 5). ## [‹bplus›] The goal of this task is to implement B+ search tree, with insertion and lookup of keys. Assume that both keys and values can be copied and that keys can be compared using ‹<› and ‹==›. The ‹max_fanout› specifies the ‘branching factor’ ⟦b⟧ of the tree: the maximum number of children a node can have. Each node then stores at most ⟦b - 1⟧ keys. As is usual with B trees, the minimum number of children for an internal node, with the exception of the root, is ⟦⌈b/2⌉⟧ (the upper integral part of ⟦b/2⟧). Each node must be stored in a single contiguous chunk of memory. That is, at most one memory allocation can be retained across an ‹insert› call (or put differently, all but at most one memory chunk allocated during ‹insert› must be freed before ‹insert› returns). template< typename key_t, typename value_t, int max_fanout > /* C */ struct bplus { Insert an element, maintaining the invariants of the B+ tree. Must run in worst-case logarithmic time. Return ‹true› if the tree was changed. bool insert( const key_t &, const value_t & ); /* C */ Look up elements. The ‹at› method should throw ‹std::out_of_range› if the key is not present in the tree. The indexing operator should insert a default-constructed value if the key is absent, and return a reference to this value. bool contains( const key_t & ) const; /* C */ value_t &at( const key_t & ); const value_t &at( const key_t & ) const; value_t &operator[]( const key_t & ); Look up an element and return the path that leads to it in the tree, i.e. the index of the child node selected during lookup at each level. Return an empty path if the key is not present. The fetch operation then takes a path returned by ‹path› and fetches the corresponding value from the tree. Please note that the paths must reflect the layout of a correct B+ tree. using path_t = std::vector< int >; /* C */ path_t path( const key_t & ) const; const value_t &fetch( const path_t &path ) const; }; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ## [‹linalg›] In this task, you will implement a few bits of basic linear algebra on top of an arbitrary scalar field, given as a template parameter. Your solution will be tested with the ‹complex› and ‹real› classes from task set 2 (which you will submit along with this solution, see below) and also using a reference implementation of those classes. Implement these 2 data types: ‹vector› and ‹matrix› (please try to avoid confusing ‹vector› with ‹std::vector›). In addition to methods prescribed below, implement the following operators: • vector addition and subtraction (operators ‹+› and ‹-›), • multiplication of a vector by a scalar (from both sides, ‹*›), • dot product of two vectors (operator ‹*›), • matrix addition (operator ‹+›), • multiplication of a vector by a matrix (again ‹*›, both sides), • multiplication of compatible matrices (again ‹*›), • multiplication of matrix by a scalar (‹*› once more), • equality on both vectors and matrices, • indexing of vectors to get or change their entries, • indexing of matrices to get or change their rows. Note: you need to submit a working version of the ‘complex’ task from the task set 2 along with this one, including the solution of ‘natural’ if applicable (though ‘natural’ is not directly required, so only include it if your solution of task ‘complex’ needs it). Add a copy of the relevant files to this directory before you submit. Only the exact arithmetic part of ‘complex’ is required: the approximation part (‹abs›, ‹arg›, ‹exp›, ‹log1p›) can be left out. You should also add this (explicit) constructor if you don't have one: ‹complex( int v )›. // extra files: complex.hpp complex.cpp natural.hpp natural.cpp /* C */ #include "complex.hpp" /* required! */ /* C */ template< typename scalar_ > /* C */ struct vector { using scalar = scalar_; explicit vector( int dimension ); /* construct a zero vector */ /* C */ explicit vector( const std::vector< scalar > & ); int dim() const; /* return the dimension */ /* C */ }; template< typename scalar_ > /* C */ struct matrix { using scalar = scalar_; using vector = ::vector< scalar >; matrix( int rows, int columns ); /* construct a zero matrix */ /* C */ explicit matrix( const std::vector< vector > &rows ); The following two methods give the user direct access to the values stored in the matrix (through column and row vectors). The ‹n› is a 0-based index, starting from top (row) or left (column). You may return ‹const› references if appropriate. vector row( int n ) const; /* C */ vector col( int n ) const; int cols() const; /* C */ int rows() const; Compute basic properties of matrices. int rank() const; /* C */ scalar det() const; /* determinant */ matrix inv() const; /* inverse matrix */ matrix transpose() const; /* transpose matrix */ Performs in-place Gaussian elimination: after the call, the matrix should be in a reduced row echelon form. void gauss(); /* C */ }; Note: the behaviour is undefined if the ‹vector› instances passed to a ‹matrix› constructor are not all of the same dimension and when ‹det› or ‹inv› are called on a non-square matrix or ‹inv› on a singular matrix. Likewise, operations on dimensionally mismatched arguments are undefined. All dimensions must be positive. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ## [‹lisp›] In this task, you will implement a simple programming language interpreter: the syntax and semantics will be based on LISP. For simplicity, the only data types will be numbers, symbols and cons cells. Accept a string that corresponds to the ‹number› non-terminal as defined below. Store the integer part of the input in ‹value› and return the number of characters processed (including the discarded decimal part). If the input is invalid, return 0. int from_string( std::string_view s, int &value ); /* C */ The interpreter itself. The ‹parse› and ‹eval› methods may be called any number of times on the same instance, in any order, and must not interfere with each other. template< typename number_t_ > /* C */ struct lisp { struct error_t {}; /* indicates parse or evaluation error */ struct nil_t {}; /* empty list */ struct cons_t; /* list cell */ struct lambda_t; /* lexical closure */ using number_t = number_t_; /* C */ using symbol_t = std::string; using value_t = std::variant< number_t, symbol_t, error_t, cons_t, nil_t, lambda_t >; Cons (list) cells are the basic building block of LISP programs. A list is built by putting values in ‹car›'s and the successive tails of the list in ‹cdr›'s. The last ‹cdr› of a proper list is always ‹nil_t›. struct cons_t /* C */ { std::shared_ptr< value_t > car, cdr; }; Syntax: expr = { space }, ( atom | list ), { space } ; list = '(', expr, { space, expr }, ')' ; space = ' ' | ? newline ? ; atom = symbol | number ; number = [ sign ], digits, [ '.', digits ] ; symbol = s_init, { s_cont } | sign ; digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ; sign = '+' | '-' ; digits = digit, { digit } ; s_init = s_char | s_symb ; s_char = ? alphabetic character ? ; s_symb = '!' | '$' | '%' | '&' | '*' | '/' | ':' | '<' | '=' | '>' | '?' | '_' | '~' ; s_cont = s_init | digit | s_spec ; s_spec = '+' | '-' | '.' | '@' | '#' ; If the input string does not conform to the above grammar, return a value of type ‹error_t›. Otherwise, the result is one of ‹number_t›, ‹symbol_t›, ‹cons_t› or ‹nil_t›. A ‹list› non-terminal is always parsed as a proper list or a ‹nil_t›. Assume that if you have ‹number_t n›, it is possible to call ‹from_string( "…", n )› with the above semantics (possibly extended to also handle the decimal part). value_t parse( std::string_view expr ); /* C */ Semantics: • numbers, nils and lambdas evaluate to themselves, • symbols evaluate to: ◦ their bound value in the current lexical environment, ◦ ‹error_t› if they are unbound, • lists are evaluated in 3 modes: ◦ ‹()› to a ‹nil_t›, ◦ as the ‹if›, ‹let› or ‹lambda› special form, ◦ as a closure invocation ‹(symbol arg₁ … argₙ)›. Special forms: • ‹(if cond expr₁ expr₂)›: ◦ evaluate to ‹expr₁› if ‹cond› evaluates to non-zero, ◦ evaluate to ‹expr₂› if ‹cond› evaluates to zero, ◦ evaluate to ‹error› otherwise, • ‹(let (name value) expr)›: evaluate to ‹expr›, in a lexical environment in which ‹name› is bound to ‹value› recursively (i.e. if ‹value› is a lambda, it may call itself using ‹name›), • ‹(lambda list expr)› evaluates to ‹lambda_t› (an anonymous closure) with names of formal arguments given by ‹list›. Closure invocation: • the symbol in the ‹car› position of the list must evaluate to a ‹lambda_t› (the result is ‹error_t› otherwise), • the entire list then evaluates the body of the lambda in the lexical environment in which the closure was defined, • extended by binding each formal argument to the corresponding ‹argₙ›, evaluated in the «current» lexical environment; • if the number of actual arguments does not match the number of formal arguments, the entire list evaluates to ‹error›. The top-level lexical environment is empty with the exception of following «builtin functions»: • ‹+›, ‹-›, ‹*›, ‹/› which each accepts exactly 2 operands of type ‹number_t› and evaluates to the obvious thing, • ‹car› and ‹cdr› which expect exactly one value of type ‹cons_t› and evaluate to its ‹car› or ‹cdr› part, • ‹cons› which accepts exactly 2 arguments and constructs a list cell out of them, • ‹list› which accepts arbitrary arguments and returns a cons list of all of them. Anything not covered above evaluates to ‹error_t›. value_t eval( value_t expr ); /* C */ }; ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ## [‹tree›] In this task, you will write a simple tree iterator, i.e. an iterator that can be used to visit all nodes of a tree, in post-order. First, implement a ‹tree› class, with the given interface. This will be the interface that ‹tree_iterator› will use (of course, you can add methods and attributes to ‹tree› as you see fit, but ‹tree_iterator› must not use them). The tree is made of nodes, where each node can have an arbitrary number of children and a single value. template< typename value_t_ > /* C */ struct tree { using value_t = value_t_; Substitute for any type you like, but make sure it can be copied, assigned and compared for equality; again, ‹tree_iterator› must not rely on the details (it can assign and copy values of type ‹node_ref› though). struct node_ref; /* C */ These functions provide access to the tree and the values stored in nodes. bool empty() const; /* C */ node_ref root() const; node_ref child_at( node_ref, int ) const; int child_count( node_ref ) const; const value_t &value( node_ref ) const; /* C */ value_t &value( node_ref ); Finally, methods for constructing and updating the tree follow. A tree iterator must not use them. The child added last has the highest index. node_ref make_root( const value_t &value ); /* C */ node_ref add_child( node_ref parent, const value_t &value ); Remove the entire subtree rooted at ‹node›. void erase_subtree( node_ref node ); /* C */ }; Iterate a given tree in post-order; ‹tree_iterator› must be (at least) a forward iterator. Adding nodes to the tree must not invalidate any iterators. Removal of a node invalidates the iterators pointing at that node or at any of its right siblings. Dereferencing the iterator yields the «value» of the node being pointed at. The tree given may or may not be an instance of the above class template ‹tree›, but it will have a ‹node_ref› nested type and the access methods (‹empty›, ‹root›, ‹child_at›, ‹child_count› and ‹value›). The ‹value› method might return a reference (like in your implementation above) or a value, and the iterator must preserve the return type of ‹value› when dereferenced. Note: You don't have to keep the exact form of the following declarations, but if you decide to replace them, you must make sure that the new declarations are equivalent in this sense: • it must be possible to declare values using ‹tree_iterator› and ‹const_tree_iterator› template instances as their type, • it is permissible to call ‹tree_begin› and ‹tree_end› on both ‹const› and mutable references to the respective ‹tree› type and the result type must match the below declarations. template< typename tree > struct tree_iterator; /* C */ template< typename tree > struct const_tree_iterator; template< typename tree > /* C */ tree_iterator< tree > tree_begin( tree & ); template< typename tree > tree_iterator< tree > tree_end( tree & ); template< typename tree > /* C */ const_tree_iterator< tree > tree_begin( const tree & ); template< typename tree > const_tree_iterator< tree > tree_end( const tree & ); # P. Practice Exams This directory contains 2 practice exams, which you can submit freely. When you do this for real, you will have 3.5 hours to read the spec, implement it and submit the solution. Since the tasks are expected to take you about 2 hours or less, the submission deadline will be «strict» and will not be extended. The «practice» exam can be submitted at any time, and will be evaluated immediately after submission. Unlike the real exam, it will be evaluated every time you submit (a real exam will only be evaluated once, using the last submission before the deadline). To submit a practice exam, use one of: $ pb161 submit pex_a $ pb161 submit pex_b ## 1. Exam A ### [‹xa1_tree›] Given a non-empty tree, compute some of its properties (listed below). Nodes are numbered, with root being node number 1 (do not assume anything else about node numbering). A branch is a path from the root to a leaf. Each edge is assigned a non-negative length. The length of a branch is the sum of lengths of all its edges. using node = int; /* C */ using length = int; using edge = std::pair< length, node >; using tree = std::multimap< node, edge >; Ensure that the input really is a tree, i.e. that each node is reachable by exactly 1 path from the root. All other functions can assume that ‹is_tree› holds for their input. Hint: in a tree, the number of edges is the number of nodes - 1. Beware though, the converse does not hold. bool is_tree( const tree & ); /* C */ Compute the length of the longest branch. int longest_branch( const tree & ); /* C */ Compute the length of the shortest branch. int shortest_branch( const tree & ); /* C */ Compute the average length among all branches. double average_branch( const tree & ); /* C */ What follows are «basic» test cases for your convenience. You can add additional test cases into main(): they will be «not» executed during evaluation, so it is okay to submit with broken main. However, make sure to «not» alter the prototype. Write all your code «before» main. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹xa2_rpn›] Implement function ‹rpn› which takes a single ‹std::string› as an argument. The string contains single-digit constants, operators ‹+› (addition), ‹d› (distance, i.e. absolute value of difference), ‹*› (multiplication) and ‹s› (sum of the entire stack), separated by exactly single space. If the string does not conform to this description, throw ‹parse_error›. If there are insufficient operands on the stack, throw ‹stack_empty›. In all other cases, return an integer which is the topmost value on the stack after the computation has finished. Examples: "3 2 1 s" → 6 "3 2 1 +" → 3 "3 x 1 +" → parse_error "31 1 +" → parse_error "1 +" → stack_empty "s" → 0 "" → parse_error " 1" → parse_error "1 2" → parse_error Notes: • RPN operates on a stack of numbers: constants push themselves onto the stack, operators pop their operands and push the result, • the initial stack is empty, • an empty input string is a parse error. #include /* C */ What follows are «basic» test cases for your convenience. You can add additional test cases into main(): they will be «not» executed during evaluation, so it is okay to submit with broken main. However, make sure to «not» alter the prototype. Write all your code «before» main. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹xa3_hamilton›] Implement function ‹hamilton› which takes a single non-empty directed graph and returns true iff the graph contains a Hamiltonian cycle, that is, a cycle that visits each vertex exactly once. It is okay to use a brute-force search. The input graph is given as an std::map from vertices (integers) to a list of edges: using edges = std::vector< int >; /* C */ using graph = std::map< int, edges >; The set of vertices is exactly the set of keys in the above map. The graph is ill-formed if a successor of a vertex does not appear as a key in the map. bool hamilton( const graph & ); /* C */ Here are a few hints: • if there is a Hamiltonian path in the graph, it visits each vertex: it does not matter from which vertex you start your search, • it is okay to use recursion (and it is in fact a good idea): a path which does not revisit any vertices can be completed to a Hamiltonian cycle iff at least one of the outgoing edges of the last vertex on the path either a) completes a Hamiltonian cycle, or b) extends the path to a new path which does not revisit any vertices and also can be completed to a Hamiltonian cycle. #include /* C */ What follows are «basic» test cases for your convenience. You can add additional test cases into main(): they will be «not» executed during evaluation, so it is okay to submit with broken main. However, make sure to «not» alter the prototype. Write all your code «before» main. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹xa4_shuffle›] Implement class ‹shuffle_vec›, which stores a sequence of 32-bit words (like an ‹std::vector› of ‹uint32_t›), but which provides access to half-words which alternate between all the odd and all the even bits in each word. In other words, iterating a sequence of 3 half-words will yield these ‹uint16_t› values: 1. even bits of the first word, 2. odd bits of the first word, 3. even bits of the second word. Same holds for indexed access. New half-words are added to the end of the sequence using a ‹push_back› method. In addition to half-word access (which uses the standard ‹begin›, ‹end› and the indexing operator), the container should also provide read-only full-word access to the data, using methods ‹raw_begin› and ‹raw_end›. If the sequence contains an odd number of half-words, the odd bits of the last full word should be all set to 0. The least-significant bit is even, i.e. the bit pattern of each byte is ‹10101010›, where ‹1› is odd and ‹0› is even. The half-words are constructed as follows: • if full word is ‹abcdefgh ijklmnop qrstuwvx αβγδεζηθ› • the even half-word is ‹bdfhjlnp rtwxβδζθ› and, • the odd half-word is ‹acegikmo qsuvαγεη›. #include /* C */ #include #include What follows are «basic» test cases for your convenience. You can add additional test cases into main(): they will be «not» executed during evaluation, so it is okay to submit with broken main. However, make sure to «not» alter the prototype. Write all your code «before» main. ## 1. Exam B ### [‹xb1_graph›] You are given a non-empty undirected graph (represented as an adjacency list, where for each edge A → B, the edge B → A is also present) and a map, which assigns a weight to each vertex. A connected component of an undirected graph is a maximal subgraph where each vertex is reachable from every other vertex. The weight of a component is the sum of weights of all its vertices. Connected components are pairwise disjoint. using graph = std::map< int, std::vector< int > >; /* C */ Assigns a weight to each vertex. You can assume that each vertex is present in the map. using weights = std::map< int, int >; /* C */ Compute the weight of the lightest component in a given graph with a given weight mapping (where lightest = smallest total weight). int lightest_component( const graph &g, const weights &w ); /* C */ Like above, but take the heaviest component. int heaviest_component( const graph &g, const weights &w ); /* C */ Compute the average weight of a component in a given graph with a given weight mapping. double average_component( const graph &g, const weights &w ); /* C */ What follows are «basic» test cases for your convenience. You can add additional test cases into main(): they will be «not» executed during evaluation, so it is okay to submit with broken main. However, make sure to «not» alter the prototype. Write all your code «before» main. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹xb2_parse›] Implement a parser for a simple language, which has atoms (single alphabetic character) and lists, written using parentheses around space-delimited elements (which can be either atoms or lists). A few examples of valid inputs: • x • (x) • (x y) • (x (x y)) And so on and so forth. Write a single function ‹parse› which accepts an ‹std::string›, with the following properties: • the function throws an exception iff the input is ill-formed • the return value is an object specified below The object returned by ‹parse› should provide the following interface (all methods are ‹const›): • ‹is_list› which returns a boolean, • ‹items› which returns an object which can be iterated (UB if ‹is_list› is false), • ‹name› which returns a ‹char› (UB if ‹is_list› is true). Dereferencing an iterator which belongs to the object returned by ‹items› should yield another object with the interface above. The formal grammar, in EBNF: item = atom | list; list = '(', items, ')' | '()' ; items = { item, ' ' }, item ; atom = ? character x iff std::isalpha( x ) ? ; #include /* C */ #include What follows are «basic» test cases for your convenience. You can add additional test cases into main(): they will be «not» executed during evaluation, so it is okay to submit with broken main. However, make sure to «not» alter the prototype. Write all your code «before» main. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹xb3_colours›] Write a function to compute the smallest possible number of colours that can colour a given graph. A correct colouring is such that no edge connects vertices with the same colour. The graph is given as a set of edges. Edges are represented as pairs, and if ⟦(u, v)⟧ is a part of the graph, so is ⟦(v, u)⟧. using graph = std::set< std::pair< int, int > >; /* C */ int colours( const graph &g ); /* C */ What follows are «basic» test cases for your convenience. You can add additional test cases into main(): they will be «not» executed during evaluation, so it is okay to submit with broken main. However, make sure to «not» alter the prototype. Write all your code «before» main. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹xb4_packed›] Write a bit-packed container, ‹oct_vector›, that will hold individually addressable octal digits (3-bit numbers) with a vector-like interface. For a capacity ⟦c = 21w⟧ (where ⟦c⟧ is the number of octal digits), ‹oct_vector› must not use more than ⟦8w⟧ (that is ⟦8c/21⟧) bytes of storage (not counting constant overhead). When calling ‹reserve›, the total capacity may be rounded to the nearest higher multiple of 21 (i.e. to a capacity such that ⟦w⟧ is an integer). To store the data, use an ‹std::vector›, and make this vector available (as a ‹const› reference) via ‹data()›. Make it possible to construct an ‹oct_vector› from such a ‹data› vector and the size of the original ‹oct_vector› from which the data came. The element type of the vector is not important. Minimal interface: indexing (with assignment), ‹push_back›, ‹reserve›, ‹capacity›, ‹pop_back›. Iterators (‹begin›, ‹end›) which can be used in a range for and with ‹std::distance› (a subset of the interface of an input iterator will do, with the iterator typedefs, dereference, pre-increment and (in)equality). struct oct_vector; /* C */ What follows are «basic» test cases for your convenience. You can add additional test cases into main(): they will be «not» executed during evaluation, so it is okay to submit with broken main. However, make sure to «not» alter the prototype. Write all your code «before» main. # S. Exercise Solutions ## 1. Week 1 ### [‹01.e1_predicates›] #define UNIT e1_predicates #include "test_main.cpp" bool all_odd( const std::vector< int > &v ) /* C */ { for ( int x : v ) if ( x % 2 != 1 ) return false; return true; } bool any_odd( const std::vector< int > &v ) /* C */ { for ( int x : v ) if ( x % 2 == 1 ) return true; return false; } bool count_divisible( const std::vector< int > &v, int k, int n ) /* C */ { for ( int x : v ) if ( x % k == 0 ) n -= 1; return n <= 0; } ### [‹01.e2_palindrome›] #define UNIT e2_palindrome #include "test_main.cpp" bool is_palindrome( const std::string &s ) /* C */ { for ( int i = 0; i < int( s.size() ); ++i ) if ( s[ i ] != s[ s.size() - i - 1 ] ) return false; return true; } ### [‹01.e3_pascal›] #define UNIT e3_pascal #include "test_main.cpp" std::vector< int > pascal( int n ) /* C */ { n --; std::vector< int > p; /* C */ p.push_back( 1 ); /* n over 0 */ for ( int k = 1; k <= n; ++ k ) /* n over 1 … n */ /* C */ p.push_back( p.back() * ( n - k + 1 ) / k ); return p; /* C */ } ### [‹01.r1_wrap›] #define UNIT r1_wrap #include "test_main.cpp" std::string fill( const std::string &in, int columns ) /* C */ { std::string out; int col = 0; for ( char c : in ) /* C */ if ( std::isblank( c ) && col >= columns ) out += '\n', col = 0; else if ( c == '\n' ) out += "\n\n", col = 0; else out += c, ++ col; return out; /* C */ } ### [‹01.r2_digits›] #define UNIT r2_digits #include "test_main.cpp" std::vector< int > digits( int n, int base ) /* C */ { assert( n >= 0 ); std::vector< int > ds; while ( n > 0 ) /* C */ { ds.push_back( n % base ); n /= base; } for ( int i = 0; i < int( ds.size() / 2 ); ++ i ) /* C */ std::swap( ds[ i ], ds[ ds.size() - i - 1 ] ); return ds; /* C */ } ### [‹01.r3_sieve›] #define UNIT r3_sieve #include int sieve( int bound ) /* C */ { std::vector< bool > s; s.resize( bound + 1, true ); for ( int i = 2; i <= bound; ++i ) /* C */ if ( s[ i ] ) for ( int j = i + i; j <= bound; j += i ) s[ j ] = false; for ( int i = bound; i > 0; --i ) /* C */ if ( s[ i ] ) return i; return 0; /* C */ } #include "test_main.cpp" /* C */ ### [‹01.r4_bsearch›] #define UNIT r4_bsearch #include using intvec = std::vector< int >; /* C */ intvec::iterator bsearch( intvec &vec, int val ) /* C */ { auto b = vec.begin(), e = vec.end(); while ( b < e ) /* the search interval is not empty */ /* C */ { auto mid = b + ( e - b ) / 2; if ( val < *mid ) e = mid; /* must be in [b, mid) */ if ( val > *mid ) b = mid + 1; /* must be in (mid, e) */ if ( val == *mid ) return mid; /* we found it */ } return vec.end(); /* C */ } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹01.r5_qsort›] #define UNIT r5_qsort #include #include int partition( std::vector< int > &vec, int low, int high, int pivot ) /* C */ { while ( vec[ low ] < pivot ) /* the pivot must be in there */ ++ low; int p_index = low; /* C */ shuffle anything < pivot to the front while remembering where (in the second half) we stashed the pivot itself for ( int i = low + 1; i < high; ++i ) /* C */ { if ( vec[ i ] < pivot ) std::swap( vec[ low++ ], vec[ i ] ); if ( vec[ i ] == pivot ) p_index = i; } put the pivot in its place between the partitions std::swap( vec[ p_index ], vec[ low ] ); /* C */ return low; /* C */ } void quicksort_range( std::vector< int > &vec, int low, int high ) /* C */ { if ( high - low <= 1 ) return; int pivot = vec[ low ]; /* whatever */ /* C */ int p_index = partition( vec, low, high, pivot ); quicksort_range( vec, low, p_index ); quicksort_range( vec, p_index + 1, high ); } void quicksort( std::vector< int > &vec ) /* C */ { quicksort_range( vec, 0, vec.size() ); } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹01.r6_radix›] #define UNIT r6_radix #include #include "test_main.cpp" int digit( unsigned num, int digit, int base ) /* C */ { while ( digit --> 0 ) num /= base; return num % base; } int digit_count( unsigned num, int base ) /* C */ { int digits = 0; while ( num ) /* C */ { ++ digits; num /= base; } return digits; /* C */ } void sort_by_digit( std::vector< unsigned > &to_sort, /* C */ int position, int base ) { std::vector< int > b_size( base, 0 ), b_start( base, 0 ), b_index( base, 0 ); std::vector< unsigned > sorted( to_sort.size() ); /* C */ for ( int num : to_sort ) /* C */ b_size[ digit( num, position, base ) ] ++; for ( int i = 1; i < base; ++i ) /* C */ b_start[ i ] = b_start[ i - 1 ] + b_size[ i - 1 ]; for ( int num : to_sort ) /* C */ { int d = digit( num, position, base ); sorted[ b_start[ d ] + b_index[ d ] ] = num; ++ b_index[ d ]; } std::swap( to_sort, sorted ); /* C */ } void radixsort( std::vector< unsigned > &to_sort, int base ) /* C */ { if ( to_sort.empty() ) return; unsigned max = *std::max_element( to_sort.begin(), to_sort.end() ); /* C */ int max_digits = digit_count( max, base ); for ( int d = 0; d < max_digits; ++d ) /* C */ sort_by_digit( to_sort, d, base ); } ## 2. Week 2 ### [‹02.e1_fibonacci›] #define UNIT e1_fibonacci #include void fibonacci( std::vector< int > &v, int n ) /* C */ { v.clear(); if ( n > 0 ) v.push_back( 1 ); /* C */ if ( n > 1 ) v.push_back( 1 ); for ( int i = 2; i < n; ++ i ) /* C */ v.push_back( v[ i - 1 ] + v[ i - 2 ] ); } #include "test_main.cpp" /* C */ ### [‹02.e2_normalize›] #define UNIT e2_normalize #include /* swap */ #include /* min, max */ void normalize( int &p, int &q ) /* C */ { int a = std::max( p, q ), b = std::min( p, q ); while ( b > 0 ) /* C */ { a = a % b; std::swap( a, b ); } p /= a; /* C */ q /= a; } #include "test_main.cpp" /* C */ ### [‹02.e3_accumulate›] #define UNIT e3_accumulate #include auto accumulate = []( auto f, const std::vector< int > &vec ) /* C */ { int sum = 0; for ( int x : vec ) /* C */ sum += f( x ); return sum; /* C */ }; #include "test_main.cpp" /* C */ ### [‹02.r1_euler›] #define UNIT r1_euler #include "test_main.cpp" long phi( long n ) /* C */ { long r = n; long p = 2; while ( p <= n ) /* C */ { if ( n % p == 0 ) { r *= p - 1; r /= p; } while ( n % p == 0 ) /* C */ n /= p; ++ p; /* C */ } return r; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹02.r2_approx›] #define UNIT r2_approx #include auto approx = []( auto f, double initial, double prec ) /* C */ { double x = f( initial ), y; do { y = x; x = f( x ); } while ( std::fabs( x - y ) > prec ); return x; /* C */ }; double golden( double prec ) /* C */ { int a = 1, b = 1; auto improve = [&]( double ) /* C */ { int c = a + b; a = b; b = c; return double( b ) / a; }; return approx( improve, 1, prec ); /* C */ } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹02.r3_solve›] #define UNIT r3_solve #include "test_main.cpp" #include bool recurse( int pos, std::vector< bool > &visited, /* C */ const std::vector< int > &jumps ) { if ( pos == int( jumps.size() ) ) { int cnt = std::count( visited.begin(), visited.end(), true ); return int( jumps.size() ) == cnt; } if ( pos < 0 || pos >= int( visited.size() ) || visited[ pos ] ) /* C */ return false; visited[ pos ] = true; /* C */ bool won = recurse( pos - jumps[ pos ], visited, jumps ) || recurse( pos + jumps[ pos ], visited, jumps ); visited[ pos ] = false; return won; } bool solve( std::vector< int > jumps ) /* C */ { std::vector< bool > visited( jumps.size(), false ); return recurse( 0, visited, jumps ); } ### [‹02.r4_sort›] #define UNIT r4_sort #include #include auto selectsort = []( std::vector< int > &to_sort, auto cmp ) /* C */ { for ( auto i = to_sort.begin(); i != to_sort.end(); ++ i ) std::swap( *i, *std::min_element( i, to_sort.end(), cmp ) ); }; #include "test_main.cpp" /* C */ ### [‹02.r5_permute›] #define UNIT r5_permute #include "test_main.cpp" #include unsigned from_digits( const std::vector< unsigned > &digits, int base ) /* C */ { unsigned r = 0; for ( unsigned d : digits ) /* C */ { r *= base; r += d; } return r; /* C */ } std::vector< unsigned > permute_digits( unsigned n, int base ) /* C */ { std::set< unsigned > r; auto digits = to_digits( n, base ); std::sort( digits.begin(), digits.end() ); do /* C */ r.insert( from_digits( digits, base ) ); while ( std::next_permutation( digits.begin(), digits.end() ) ); return std::vector< unsigned >( r.begin(), r.end() ); /* C */ } ### [‹02.r6_bsearch›] #define UNIT r6_bsearch #include auto search = []( std::vector< int > &vec, int val, auto cmp ) /* C */ { auto b = vec.begin(), e = vec.end(); while ( b < e ) /* the search interval is not empty */ /* C */ { auto mid = b + ( e - b ) / 2; if ( cmp( val, *mid ) ) e = mid; /* must be in [b, mid) */ else if ( cmp( *mid, val ) ) b = mid + 1; /* must be in (mid, e) */ else return mid; /* we found it */ } return vec.end(); /* C */ }; #include "test_main.cpp" /* C */ ## 3. Week 3 ### [‹03.e1_unique›] #define UNIT e1_unique #include #include "test_main.cpp" std::vector< int > unique( const std::vector< int > &v ) /* C */ { std::vector< int > out; std::set< int > seen; for ( int x : v ) /* C */ if ( !seen.count( x ) ) { out.push_back( x ); seen.insert( x ); } return out; /* C */ } ### [‹03.e2_reflexive›] #define UNIT e2_reflexive #include "test_main.cpp" relation reflexive( const relation &r ) /* C */ { relation out = r; for ( auto [ x, y ] : r ) /* C */ { out.emplace( x, x ); out.emplace( y, y ); } return out; /* C */ } ### [‹03.e3_normalize›] #define UNIT e3_normalize #include "test_main.cpp" signal_t normalize( const signal_t &s ) /* C */ { double m = 0; signal_t out; for ( double x : s ) /* C */ m = std::max( m, x ); if ( m == 0 ) /* C */ m = 1; for ( double x : s ) /* C */ out.push_back( x / m ); return out; /* C */ } ### [‹03.r1_mode›] #define UNIT r1_mode #include "test_main.cpp" #include int mode( const std::vector< int > &in ) /* C */ { std::map< int, int > freq; int max_val = 0, max_freq = 0; for ( int x : in ) /* C */ freq[ x ] ++; for ( auto [ v, f ] : freq ) /* C */ if ( f > max_freq ) { max_val = v; max_freq = f; } return max_val; /* C */ } ### [‹03.r2_buckets›] #define UNIT r2_buckets #include "test_main.cpp" #include std::vector< int > sort( const std::vector< int > &stones, /* C */ const std::vector< bucket > &buckets ) { std::vector< int > out( buckets.size(), 0 ); for ( int s : stones ) /* C */ for ( size_t i = 0; i < buckets.size(); ++ i ) { auto [ min, max ] = buckets[ i ]; if ( s >= min && s <= max ) /* C */ out[ i ] += s; } return out; /* C */ } ### [‹03.r3_shortest›] #define UNIT r3_shortest #include "test_main.cpp" #include std::map< int, int > shortest( const graph &g, int initial ) /* C */ { std::map< int, int > dist; std::queue< int > queue; queue.push( initial ); dist[ initial ] = 0; while ( !queue.empty() ) /* C */ { int from = queue.front(); queue.pop(); for ( auto to : g.at( from ) ) /* C */ { if ( dist.count( to ) ) continue; dist[ to ] = dist[ from ] + 1; /* C */ queue.push( to ); } } return dist; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹03.r4_flood›] #define UNIT r4_flood #include #include "test_main.cpp" int flood( const grid &pixels, int width, /* C */ int x0, int y0, bool fill ) { grid work = pixels; int count = 0; int height = pixels.size() / width; std::queue< std::pair< int, int > > todo; auto flip = [&]( int x, int y ) /* C */ { int idx = y * width + x; if ( x >= 0 && x < width && /* C */ y >= 0 && y < height && work[ idx ] != fill ) { todo.emplace( x, y ); work[ idx ] = fill; ++ count; } }; flip( x0, y0 ); /* C */ while ( !todo.empty() ) /* C */ { auto [ x, y ] = todo.front(); todo.pop(); for ( int dx : { -1, 0, 1 } ) /* C */ for ( int dy : { -1, 0, 1 } ) if ( dx || dy ) flip( x + dx, y + dy ); } return count; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹03.r5_colour›] #define UNIT r5_colour #include /* C */ #include #include using graph = std::set< std::pair< int, int > >; /* C */ bool has_3colouring( const graph &g, std::map< int, int8_t > &colouring ) /* C */ { for ( auto [ u, v ] : g ) if ( colouring[ u ] && colouring[ u ] == colouring[ v ] ) return false; for ( auto [ u, v ] : g ) /* C */ if ( !colouring[ v ] ) { for ( int c = 1; c <= 3; ++ c ) if ( colouring[ u ] != c ) { colouring[ v ] = c; if ( has_3colouring( g, colouring ) ) return true; colouring[ v ] = 0; } return false; /* C */ } return true; /* C */ } bool has_3colouring( const graph &g ) /* C */ { std::map< int, int8_t > colouring{ { g.begin()->first, 1 } }; return has_3colouring( g, colouring ); } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹03.r6_life›] #define UNIT r6_life #include "test_main.cpp" bool updated( int x, int y, const grid &cells ) /* C */ { int count = 0; bool alive = cells.count( { x, y } ); for ( int dx : { -1, 0, 1 } ) /* C */ for ( int dy : { -1, 0, 1 } ) if ( dx || dy ) count += cells.count( { x + dx, y + dy } ); return alive ? count == 2 || count == 3 : count == 3; /* C */ } grid life( const grid &cells, int n ) /* C */ { if ( n == 0 ) return cells; grid todo, ngen; /* C */ for ( auto [ x, y ] : cells ) /* C */ for ( int dx : { -1, 0, 1 } ) for ( int dy : { -1, 0, 1 } ) todo.emplace( x + dx, y + dy ); for ( auto [ x, y ] : todo ) /* C */ if ( updated( x, y, cells ) ) ngen.emplace( x, y ); return life( ngen , n - 1 ); /* C */ } ## 4. Week 4 ### [‹04.e1_diameter›] #define UNIT e1_diameter #include struct point /* C */ { double x, y; point( double x, double y ) : x( x ), y( y ) {} }; struct circle_radius /* C */ { point center; double radius; circle_radius( point c, double r ) : center( c ), radius( r ) {} }; struct circle_point /* C */ { point center, perimeter; circle_point( point c, point p ) : center( c ), perimeter( p ) {} }; double diameter( const circle_radius &c ) /* C */ { return c.radius * 2; } double diameter( const circle_point &c ) /* C */ { double dx = c.center.x - c.perimeter.x; double dy = c.center.y - c.perimeter.y; return 2 * std::sqrt( dx * dx + dy * dy ); } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹04.e2_circle›] #define UNIT e2_circle #include struct point /* C */ { double x, y; point( double x, double y ) : x( x ), y( y ) {} }; struct circle /* C */ { point center; double radius; circle( point c, double r ) /* C */ : center( c ), radius( r ) {} circle( point c, point p ) /* C */ : center( c ), radius( std::sqrt( std::pow( p.x - c.x, 2 ) + std::pow( p.y - c.y, 2 ) ) ) {} }; #include "test_main.cpp" /* C */ ### [‹04.e3_index›] #define UNIT e3_index #include #include int &element( std::vector< int > &v, int idx ) /* C */ { return v[ idx ]; } int element( const std::vector< int > &v, int idx ) /* C */ { return v[ idx ]; } int &element( std::pair< int, int > &v, int idx ) /* C */ { return idx == 0 ? v.first : v.second; } int element( const std::pair< int, int > &v, int idx ) /* C */ { return idx == 0 ? v.first : v.second; } int size( const std::pair< int, int > & ) { return 2; } /* C */ int size( const std::vector< int > &v ) { return v.size(); } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹04.r1_complex›] #define UNIT r1_complex #include struct angle { double v; }; /* C */ struct complex /* C */ { double real, imag; complex( double r, double i ) /* C */ : real( r ), imag( i ) {} complex( double m, angle phi ) /* C */ : real( m * std::cos( phi.v ) ), imag( m * std::sin( phi.v ) ) {} }; double magnitude( double x ) /* C */ { return std::fabs( x ); } double norm( complex c ) /* C */ { return c.real * c.real + c.imag * c.imag; } double magnitude( complex c ) /* C */ { return std::sqrt( norm( c ) ); } double reciprocal( double x ) /* C */ { return 1 / x; } complex reciprocal( complex c ) /* C */ { return complex( c.real / norm( c ), -c.imag / norm( c ) ); } double arg( complex c ) /* C */ { return std::atan2( c.real, c.imag ); } double real( complex c ) { return c.real; } /* C */ double imag( complex c ) { return c.imag; } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹04.r2_bsearch›] #define UNIT r2_bsearch #include using intvec = std::vector< int >; /* C */ int bsearch_idx( const intvec &vec, int val ) /* C */ { auto b = vec.begin(), e = vec.end(); while ( b < e ) /* the search interval is not empty */ /* C */ { auto mid = b + ( e - b ) / 2; if ( val < *mid ) e = mid; /* must be in [b, mid) */ if ( val > *mid ) b = mid + 1; /* must be in (mid, e) */ if ( val == *mid ) return std::distance( vec.begin(), mid ); /* we found it */ } return vec.size(); /* C */ } auto bsearch( const intvec &vec, int val ) /* C */ { return std::next( vec.begin(), bsearch_idx( vec, val ) ); } auto bsearch( intvec &vec, int val ) /* C */ { return std::next( vec.begin(), bsearch_idx( vec, val ) ); } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹04.r3_search›] #define UNIT r3_search #include struct node /* C */ { int value; int left = -1, right = -1; node( int v ) : value( v ) {} }; using node_pool = std::vector< node >; /* C */ class node_ref /* C */ { const node_pool &pool; int idx; friend class tree; public: node_ref( const node_pool &p, int i ) : pool( p ), idx( i ) {} node_ref left() const { return { pool, pool[ idx ].left }; } node_ref right() const { return { pool, pool[ idx ].right }; } int value() const { return pool[ idx ].value; } bool valid() const { return idx >= 0; } }; class tree /* C */ { node_pool _pool; int _root = -1; public: /* C */ node_ref root() const { return { _pool, _root }; } bool empty() const { return _root == -1; } node &get( node_ref n ) { return _pool[ n.idx ]; } void insert( node_ref what, node_ref where, int &attach ) /* C */ { if ( !where.valid() ) attach = what.idx; else if ( what.value() < where.value() ) return insert( what, where.left(), get( where ).left ); else return insert( what, where.right(), get( where ).right ); } void insert( int v ) /* C */ { int id = _pool.size(); _pool.emplace_back( v ); return insert( { _pool, id }, root(), _root ); } }; /* C */ #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹04.r4_bitptr›] #define UNIT r4_bitptr #include #include class bitptr /* C */ { std::byte *_ptr; int _offset; bool _valid = true; public: bitptr() : _valid( false ) {} bitptr( std::byte *p, int o ) : _ptr( p ), _offset( o ) { assert( o >= 0 && o <= 7 ); } static auto one() { return std::byte( 1 ); } /* C */ bool get() const { return bool( *_ptr & one() << _offset ); } /* C */ void set( bool b ) const { *_ptr = ( *_ptr & ~( one() << _offset ) ) | std::byte( b ) << _offset; } void advance( int n = 1 ) { _offset += n; _ptr += _offset / 8; _offset %= 8; } /* C */ bool valid() const { return _valid; } }; class const_bitptr /* C */ { bitptr _ptr; public: const_bitptr() = default; const_bitptr( const std::byte *p, int o ) : _ptr( const_cast< std::byte * >( p ), o ) {} bool get() const { return _ptr.get(); } /* C */ void set( bool b ) const { return _ptr.set( b ); } void advance( int n = 1 ) { _ptr.advance( n ); } bool valid() const { return _ptr.valid(); } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹04.r6_sort›] #define UNIT r6_sort #include #include #include int *partition( int *low, const int *high, int pivot ) /* C */ { while ( *low < pivot ) /* the pivot must be in there */ ++ low; int *p_ptr = low; /* C */ shuffle anything < pivot to the front while remembering where (in the second half) we stashed the pivot itself for ( int *p = low + 1; p < high; ++p ) /* C */ { if ( *p < pivot ) std::swap( *low++, *p ); if ( *p == pivot ) p_ptr = p; } put the pivot in its place between the partitions std::swap( *p_ptr, *low ); /* C */ return low; /* C */ } void quicksort( int *low, int *high ) /* C */ { if ( high - low <= 1 ) return; int pivot = *low; /* whatever */ /* C */ int *p_ptr = partition( low, high, pivot ); quicksort( low, p_ptr ); quicksort( p_ptr + 1, high ); } void sort( std::vector< int > &vec ) /* C */ { quicksort( &*vec.begin(), &*vec.end() ); } void sort( std::list< int > &list ) /* C */ { if ( std::size( list ) <= 1 ) return; std::list< int > half; /* C */ half.splice( half.begin(), list, /* C */ list.begin(), std::next( list.begin(), list.size() / 2 ) ); sort( half ); /* C */ sort( list ); list.merge( half ); } #include "test_main.cpp" /* C */ ## 5. Week 5 ### [‹05.e1_cartesian›] #define UNIT e1_cartesian This is a solution that uses the friend syntax. For a solution which uses the method syntax, see ‹complex.alt.cpp›. class complex /* C */ { double real, imag; public: complex( double r, double i ) : real( r ), imag( i ) {} friend complex operator+( complex a, complex b ) /* C */ { You may not know this syntax yet. In a return statement, braces without a type name call the constructor of the return type. I.e. ‹{ a, b }› in this context is the same as ‹complex( a, b )›. return { a.real + b.real, a.imag + b.imag }; /* C */ } friend complex operator-( complex a, complex b ) /* C */ { return { a.real - b.real, a.imag - b.imag }; } friend complex operator-( complex a ) /* C */ { return { -a.real, -a.imag }; } friend bool operator==( complex a, complex b ) /* C */ { return a.real == b.real && a.imag == b.imag; } }; To avoid having a copy of the tests, we ‹#include› the original ‹.cpp› file here. You won't be able to compile this solution if you add your implementation to the original ‹.cpp› file, but you can probably trust us that the solution above works. #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹05.e2_force›] #define UNIT e2_force #include class force /* C */ { double x, y, z; /* cartesian components of the force */ public: force( double x, double y, double z ) : x( x ), y( y ), z( z ) {} We only define multiplication by a scalar (‹double›) from left, since we only need that here, but it would be equally valid to flip the operand types (and define scalar multiplication on the right). friend force operator*( double s, force f ) /* C */ { return { s * f.x, s * f.y, s * f.z }; } Bog-standard vector addition. friend force operator+( force a, force b ) /* C */ { return { a.x + b.x, a.y + b.y, a.z + b.z }; } Fuzzy vector equality. Two vectors are equal when all their components are equal. friend bool operator==( force a, force b ) /* C */ { return std::fabs( a.x - b.x ) < 1e-10 && std::fabs( a.y - b.y ) < 1e-10 && std::fabs( a.z - b.z ) < 1e-10; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹05.e3_forcefmt›] #define UNIT e3_forcefmt #include #include class force /* C */ { double x = 0, y = 0, z = 0; public: force( double x, double y, double z ) : x( x ), y( y ), z( z ) {} force() = default; /* C */ bool operator==( const force &f ) const /* C */ { return std::fabs( f.x - x ) < 1e-10 && std::fabs( f.y - y ) < 1e-10 && std::fabs( f.z - z ) < 1e-10; } friend std::ostream &operator<<( std::ostream &o, /* C */ const force &f ) { return o << "[" << f.x << " " << f.y << " " << f.z << "]"; } friend std::istream &operator>>( std::istream &i, force &f ) /* C */ { char ch; if ( !( i >> ch ) || ch != '[' ) /* C */ i.setstate( i.failbit ); i >> f.x >> f.y >> f.z; /* C */ if ( !( i >> ch ) || ch != ']' ) /* C */ i.setstate( i.failbit ); return i; /* C */ } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹05.r1_poly›] #define UNIT r1_poly #include class poly /* C */ { std::vector< int > cs; public: void set( int p, int c ) { cs.resize( std::max( degree(), p + 1 ), 0 ); cs[ p ] = c; } int get( int p ) const /* C */ { return p < degree() ? cs[ p ] : 0; } int degree() const { return cs.size(); } /* C */ poly operator+( const poly &o ) const /* C */ { poly rv; for ( int i = 0; i < std::max( degree(), o.degree() ); ++i ) rv.set( i, get( i ) + o.get( i ) ); return rv; } poly operator*( const poly &o ) const /* C */ { poly rv; for ( int i = 0; i < degree(); ++i ) for ( int j = 0; j < o.degree(); ++j ) rv.set( i + j, rv.get( i + j ) + get( i ) * o.get( j ) ); return rv; } bool operator==( const poly &o ) const /* C */ { for ( int i = 0; i < std::max( degree(), o.degree() ); ++i ) if ( get( i ) != o.get( i ) ) return false; return true; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹05.r2_csv›] #define UNIT r2_csv It is probably easiest to implement this using ‹std::getline› to fetch both lines and individual cells. Other approaches are certainly possible though. #include /* C */ #include #include class bad_format {}; /* C */ class csv /* C */ { std::vector< std::vector< int > > data; public: Process a single line, with some rudimentary format validation. The ‹std::stoi› call will throw if the number cannot be parsed, but will not complain about trailing garbage. void process_line( const std::string &line, int cols ) /* C */ { std::istringstream i_line( line ); std::string cell; data.emplace_back(); /* C */ for ( int i = 0; i < cols; ++i ) /* C */ { if ( !std::getline( i_line, cell, ',' ) ) throw bad_format(); data.back().push_back( std::stoi( cell ) ); } i_line.get(); /* C */ if ( !i_line.eof() ) /* C */ throw bad_format(); } The constructor, fetches lines until it reaches the end of the file and processes each of them using the above. csv( std::istream &i, int cols ) /* C */ { std::string line; while ( std::getline( i, line ) ) /* C */ process_line( line, cols ); } The indexing operator. Since we want ‹[ x ][ y ]› to work, we need to return something with an indexing operator of its own here. The easiest thing to do is to return the underlying ‹vector› in which we store the row. It would be possible to return a proxy object too. std::vector< int > &operator[]( int i ) /* C */ { return data[ i ]; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹05.r3_set›] #define UNIT r3_set #include #include class set /* C */ { std::set< int > s; public: void add( int x ) { s.insert( x ); } bool has( int x ) const { return s.find( x ) != s.end(); } set operator|( const set &o ) const /* C */ { set r; std::set_union( s.begin(), s.end(), o.s.begin(), o.s.end(), std::inserter( r.s, r.s.begin() ) ); return r; } set operator&( const set &o ) const /* C */ { set r; std::set_intersection( s.begin(), s.end(), o.s.begin(), o.s.end(), std::inserter( r.s, r.s.begin() ) ); return r; } set operator-( const set &o ) const /* C */ { set r; std::set_difference( s.begin(), s.end(), o.s.begin(), o.s.end(), std::inserter( r.s, r.s.begin() ) ); return r; } bool operator<=( const set &o ) const /* C */ { return ( *this - o ).s.empty(); } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹05.r5_json›] #define UNIT r5_json #include "test_main.cpp" #include std::string escape( const std::string &in ) /* C */ { std::ostringstream ostr; for ( char c : in ) /* C */ { if ( c == '\\' || c == '"' ) ostr << "\\"; ostr << c; } return ostr.str(); /* C */ } std::string to_json( const str_dict &dict ) /* C */ { std::ostringstream ostr; ostr << "{"; /* C */ bool comma = false; for ( auto [ k, v ] : dict ) /* C */ { ostr << ( comma ? ", " : " " ); comma = true; ostr << "\"" << escape( k ) << "\": \"" << escape( v ) << "\""; } ostr << ( comma ? " }" : "}" ); /* C */ return ostr.str(); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹05.r6_cpp›] #define UNIT r6_cpp #include "test_main.cpp" #include /* C */ #include #include #include inline auto split( const std::string &sv, char delim ) /* C */ { if ( auto offset = sv.find( delim ); offset != sv.npos ) return std::pair( sv.substr( 0, offset ), sv.substr( offset + 1, sv.npos ) ); else return std::pair( sv, std::string() ); } class preprocessor /* C */ { std::set< std::string > defs; std::stack< bool > _emit; public: std::string out; bool emit() const { return _emit.empty() || _emit.top(); } /* C */ void process( const std::string &line ) /* C */ { auto [ dir, args ] = split( line, ' ' ); if ( dir == "#ifdef" ) /* C */ _emit.push( defs.count( args ) ); if ( dir == "#endif" ) _emit.pop(); if ( emit() ) /* C */ { if ( dir == "#define" ) defs.insert( args ); if ( dir == "#undef" ) defs.erase( args ); if ( dir == "#include" ) read( args.substr( 1, args.size() - 2 ) ); } } void read( const std::string &filename ) /* C */ { std::ifstream in( filename ); /* NB. Fails quietly. */ std::string line; while ( std::getline( in, line ) ) /* C */ if ( !line.empty() && line[ 0 ] == '#' ) process( line ); else if ( emit() ) out += line + "\n"; } }; std::string cpp( const std::string &filename ) /* C */ { preprocessor p; p.read( filename ); return p.out; } ## 6. Week 6 ### [‹06.e1_default›] #define UNIT e1_default #include #include int stoi_or( std::string s, int def ) /* C */ { try { return std::stoi( s ); } catch ( std::out_of_range & ) { return def; } catch ( std::invalid_argument & ) { return def; } } #include "test_main.cpp" /* C */ ### [‹06.e2_counter›] #define UNIT e2_counter struct counted /* C */ { counted(); counted( const counted & ); ~counted(); }; #include "test_main.cpp" /* C */ counted::counted() /* C */ { ++ counter; } counted::counted( const counted & ) /* C */ { ++ counter; } counted::~counted() /* C */ { -- counter; } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹06.e3_coffee›] #define UNIT e3_coffee class token /* C */ { bool valid = false; friend class machine; public: /* C */ token() = default; token( const token & ) = delete; token( token &&o ) : valid( o.valid ) { o.valid = false; } token &operator=( const token & ) = delete; /* C */ token &operator=( token &&o ) noexcept { valid = o.valid; o.valid = false; return *this; } }; class machine /* C */ { bool busy = false; public: token make(); void fetch( token &t ); }; #include "test_main.cpp" /* C */ token machine::make() /* C */ { if ( busy ) throw ::busy(); token t; t.valid = true; busy = true; return t; } void machine::fetch( token &t ) /* C */ { assert( busy ); if ( !t.valid ) /* C */ throw invalid(); t.valid = false; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹06.r1_printing›] #define UNIT r1_printing #include #include class job /* C */ { int _id; std::string _owner; int _pages; public: /* C */ job( int id, std::string owner, int pages ) /* C */ : _id( id ), _owner( owner ), _pages( pages ) {} job( job && ) = default; /* C */ job( const job & ) = delete; job &operator=( const job & ) = delete; int id() const { return _id; } /* C */ int page_count() const { return _pages; } std::string owner() const { return _owner; } }; class queue /* C */ { std::map< int, job > _jobs; int _count = 0; public: int dequeue() /* C */ { const auto &item = *_jobs.begin(); int id = item.first; _count -= item.second.page_count(); _jobs.erase( id ); return id; } void enqueue( job &&j ) /* C */ { int id = j.id(); _count += j.page_count(); _jobs.emplace( id, std::move( j ) ); } job release( int id ) /* C */ { job rv( std::move( _jobs.at( id ) ) ); _jobs.erase( id ); _count -= rv.page_count(); return rv; } int page_count() const /* C */ { return _count; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹06.r2_bsearch›] #define UNIT r2_bsearch #include #include class token; /* C */ class flat_map /* C */ { std::vector< std::pair< std::string, token > > _data; public: bool emplace( std::string k, int v ); int index( const std::string &k ) const; int index_or_throw( const std::string &k ) const; token &at( const std::string &k ); const token &at( const std::string &k ) const; token &operator[]( const std::string &k ); }; #include "test_main.cpp" /* C */ int flat_map::index( const std::string &k ) const /* C */ { int low = 0, high = _data.size(); while ( low < high ) /* C */ { int mid = ( low + high ) / 2; if ( k < _data[ mid ].first ) /* C */ high = mid; else if ( k > _data[ mid ].first ) low = mid + 1; else return mid; } assert( low <= int( _data.size() ) ); /* C */ return low; } bool flat_map::emplace( std::string k, int v ) /* C */ { int idx = index( k ); if ( idx < int( _data.size() ) && _data[ idx ].first == k ) /* C */ return false; _data.emplace( _data.begin() + idx, std::move( k ), v ); /* C */ return true; } int flat_map::index_or_throw( const std::string &k ) const /* C */ { if ( int idx = index( k ); _data[ idx ].first == k ) return idx; else throw std::out_of_range( "indexing flat_map" ); } token &flat_map::at( const std::string &k ) /* C */ { return _data[ index_or_throw( k ) ].second; } const token &flat_map::at( const std::string &k ) const /* C */ { return _data[ index_or_throw( k ) ].second; } token &flat_map::operator[]( const std::string &k ) /* C */ { if ( int idx = index( k ); _data[ idx ].first == k ) return _data[ idx ].second; emplace( k, 0 ); /* C */ return at( k ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹06.r4_tinyvec›] #define UNIT r4_tinyvec #include #include class token; /* C */ class tiny_vector /* C */ { std::array< uint8_t, 32 > _mem; int _count = 0; public: /* C */ void insert( int idx, token &&v ); void erase( int idx ); token *slot( int i ); /* C */ const token *slot( int i ) const; const token &front() const { return *slot( 0 ); } /* C */ const token &back() const { return *slot( _count - 1 ); } token &front() { return *slot( 0 ); } /* C */ token &back() { return *slot( _count - 1 ); } ~tiny_vector(); /* C */ }; #include "test_main.cpp" /* C */ token *tiny_vector::slot( int i ) /* C */ { assert( i >= 0 ); assert( i < _count ); return reinterpret_cast< token * >( _mem.begin() ) + i; } const token *tiny_vector::slot( int i ) const /* C */ { return reinterpret_cast< const token * >( _mem.begin() ) + i; } tiny_vector::~tiny_vector() /* C */ { while ( _count ) erase( _count - 1 ); } void tiny_vector::erase( int idx ) /* C */ { std::destroy_at( slot( idx ) ); for ( int i = idx; i < _count - 1; ++i ) /* C */ { std::uninitialized_move_n( slot( i + 1 ), 1, slot( i ) ); std::destroy_at( slot( i + 1 ) ); } -- _count; /* C */ } void tiny_vector::insert( int idx, token &&v ) /* C */ { if ( _count == _mem.size() / sizeof( token ) ) throw insufficient_space(); ++ _count; /* C */ for ( int i = _count - 1; i > idx; --i ) /* C */ { std::uninitialized_move_n( slot( i - 1 ), 1, slot( i ) ); std::destroy_at( slot( i - 1 ) ); } if ( idx < _count - 1 ) /* C */ std::destroy_at( slot( idx ) ); std::uninitialized_move_n( &v, 1, slot( idx ) ); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹06.r5_lock›] #define UNIT r5_lock class mutex; /* C */ class lock /* C */ { mutex *_mutex; public: lock( mutex &m ); ~lock(); lock( const lock & ) = delete; /* C */ lock &operator=( const lock & ) = delete; lock( lock && ); /* C */ lock &operator=( lock && ); }; #include "test_main.cpp" /* C */ lock::lock( mutex &m ) /* C */ : _mutex( &m ) { _mutex->lock(); } lock::~lock() /* C */ { if ( _mutex ) _mutex->unlock(); } lock::lock( lock &&o ) /* C */ : _mutex( o._mutex ) { o._mutex = nullptr; } lock &lock::operator=( lock &&o ) /* C */ { if ( _mutex ) _mutex->unlock(); _mutex = o._mutex; o._mutex = nullptr; return *this; } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹06.r6_bounded›] #define UNIT r6_bounded #include class insufficient_space {}; /* C */ class bounded_queue /* C */ { std::vector< int > _data; int _first = 0, _count = 0; public: explicit bounded_queue( int cnt ) : _data( cnt ) {} bool empty() const { return !_count; } bool full() const { return _count == int( _data.size() ); } int next() const { return _data[ _first ]; } int pop() /* C */ { int rv = _data[ _first ++ ]; _first %= _data.size(); -- _count; return rv; } void push( int v ) /* C */ { if ( full() ) throw insufficient_space(); _data[ ( _first + _count ) % _data.size() ] = v; /* C */ ++ _count; } }; #include "test_main.cpp" /* C */ ## 7. Week 7 ### [‹07.e1_dynarray›] #define UNIT e1_dynarray #include #include class dynarray /* C */ { std::unique_ptr< int[] > _data; int _size; public: dynarray( int size ) /* C */ : _data( std::make_unique< int[] >( size ) ), _size( size ) {} void resize( int size ) /* C */ { auto d_new = std::make_unique< int[] >( size ); auto d_old = _data.get(); std::copy( d_old, d_old + std::min( size, _size ), d_new.get() ); _data = std::move( d_new ); _size = size; } int &operator[]( int i ) /* C */ { return _data[ i ]; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹07.e2_list›] #define UNIT e2_list #include class list /* C */ { int _head; std::unique_ptr< list > _tail; public: list( const list &o ) /* C */ : _head( o._head ), _tail( o._tail ? std::make_unique< list >( *o._tail ) : nullptr ) {} list( int h, const list &t ) /* C */ : _head( h ), _tail( std::make_unique< list >( t ) ) {} list() = default; /* C */ bool empty() const { return !_tail; } /* C */ int head() const { return _head; } const list &tail() const { return *_tail; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹07.r1_circular›] #define UNIT r1_circular The solution proceeds along the lines of ‹queue.cpp›: we use a singly-linked list. The solution is simpler because we do not need iteration (which was replaced by ‹rotate›. #include /* C */ A node of the data structure, bog standard. struct circular_node /* C */ { using pointer = std::unique_ptr< circular_node >; pointer next; int value; }; Like before, we remember the head of the list (as a ‹unique_ptr›) and a pointer to the last node, which we need to implement ‹rotate›. class circular /* C */ { std::unique_ptr< circular_node > head; circular_node *last = nullptr; public: bool empty() const { return !last; } /* C */ In this case, the ‹push› method works at the head, since we use the list in a stack-like order. We have already seen «move assignment», using the ‹std::move› helper function. void push( int v ) /* C */ { auto new_head = std::make_unique< circular_node >(); new_head->value = v; new_head->next = std::move( head ); head = std::move( new_head ); if ( !last ) last = head.get(); } Popping items at the head is quite simple. void pop() /* C */ { head = std::move( head->next ); if ( !head ) last = nullptr; } Access to the top element. int top() const { return head->value; } /* C */ int &top() { return head->value; } And the rotate operation: we pop a node off the head and chain it to the list at the tail end. Must not forget to update the ‹last› pointer. Does not work on empty list. void rotate() /* C */ { auto next_head = std::move( head->next ); last->next = std::move( head ); last = last->next.get(); head = std::move( next_head ); } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹07.r2_zipper›] #define UNIT r2_zipper #include #include struct node /* C */ { using ptr = std::unique_ptr< node >; int value; ptr next; node( int v, ptr n ) : value( v ), next( std::move( n ) ) {} }; class zipper /* C */ { int _focus; using node_ptr = std::unique_ptr< node >; node_ptr _left, _right; public: /* C */ zipper( int f ) : _focus( f ) {} bool shift( node_ptr &a, node_ptr &b ) /* C */ { auto new_b = std::move( b->next ); auto new_a = std::move( b ); new_a->next = std::move( a ); std::swap( new_a->value, _focus ); /* C */ b = std::move( new_b ); /* C */ a = std::move( new_a ); return true; /* C */ } void push( node_ptr &p, int v ) /* C */ { p = std::make_unique< node >( v, std::move( p ) ); } bool shift_left() /* C */ { return _left ? shift( _right, _left ) : false; } bool shift_right() /* C */ { return _right ? shift( _left, _right ) : false; } void push_left( int v ) { push( _left, v ); } /* C */ void push_right( int v ) { push( _right, v ); } int &focus() { return _focus; } /* C */ int focus() const { return _focus; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹07.r3_segment›] #define UNIT r3_segment #include #include #include struct segment_map /* C */ { struct node { std::unique_ptr< node > l, r; int div; node( int d ) : div( d ) {} }; std::unique_ptr< node > root; /* C */ int min, max; segment_map( int l, int u ) : min( l ), max( u ) {} /* C */ std::pair< int, int > query( int i, node *n, int l, int u ) const /* C */ { if ( !n ) return { l, u }; if ( i < n->div ) return query( i, n->l.get(), l, n->div ); if ( i >= n->div ) return query( i, n->r.get(), n->div, u ); std::abort(); } std::pair< int, int > query( int i ) const /* C */ { return query( i, root.get(), min, max ); } void split( int n ) /* C */ { auto old_root = std::move( root ); root = std::make_unique< node >( n ); if ( !old_root ) return; if ( old_root->div > n ) /* C */ root->r = std::move( old_root ); else root->l = std::move( old_root ); } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹07.r4_diff›] #define UNIT r4_diff #include #include struct node /* C */ { using ptr = std::shared_ptr< node >; enum op_t { cnst, var, add, mul, exp } op; int num = 0; ptr l, r; }; class expr /* C */ { public: node::ptr ptr; expr() : ptr( std::make_shared< node >() ) {} expr( int c ) : expr() { ptr->num = c; ptr->op = node::cnst; } expr( node::op_t o, node::ptr l = nullptr, node::ptr r = nullptr ) : expr() { ptr->op = o; ptr->l = l; ptr->r = r; } expr( node::ptr p ) :ptr( p ) {} friend expr expnat( expr e ) /* C */ { return { node::exp, e.ptr }; } friend expr operator+( expr a, expr b ) /* C */ { return { node::add, a.ptr, b.ptr }; } friend expr operator*( expr a, expr b ) /* C */ { return { node::mul, a.ptr, b.ptr }; } }; const expr x{ node::var }; /* C */ double eval( expr e, double v ) /* C */ { switch ( e.ptr->op ) { case node::cnst: return e.ptr->num; case node::var: return v; case node::add: return eval( e.ptr->l, v ) + eval( e.ptr->r, v ); case node::mul: return eval( e.ptr->l, v ) * eval( e.ptr->r, v ); case node::exp: return std::exp( eval( e.ptr->l, v ) ); } abort(); } expr diff( expr e ) /* C */ { switch ( e.ptr->op ) { case node::cnst: return { 0 }; case node::var: return { 1 }; case node::add: return diff( e.ptr->l ) + diff( e.ptr->r ); case node::mul: return diff( e.ptr->l ) * e.ptr->r + diff( e.ptr->r ) * e.ptr->l; case node::exp: return e * diff( e.ptr->l ); } abort(); } #include "test_main.cpp" /* C */ ## 8. Week 8 ### [‹08.e1_resistance›] #define UNIT e1_resistance class segment /* C */ { public: virtual double r() const = 0; }; class series : public segment /* C */ { double total = 0; public: void add( double r ) { total += r; } void add( const segment &s ) { total += s.r(); } double r() const override { return total; } }; class parallel : public segment /* C */ { double recip = 0; public: void add( double r ) { recip += 1.0 / r; } void add( const segment &s ) { recip += 1.0 / s.r(); } double r() const override { return 1.0 / recip; } }; double resistance( const segment &s ) /* C */ { return s.r(); } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹08.e2_perimeter›] #define UNIT e2_perimeter #include class shape /* C */ { public: virtual double perimeter() const = 0; }; class circle : public shape /* C */ { double _radius; public: circle( double r ) : _radius( r ) {} double perimeter() const override { return 8 * std::atan( 1 ) * _radius; } }; class rectangle : public shape /* C */ { double _width, _height; public: rectangle( double w, double h ) : _width( w ), _height( h ) {} double perimeter() const override { return 2 * _width + 2 * _height; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹08.e3_fight›] #define UNIT e3_fight class rock; /* C */ class paper; class scissors; class gesture /* C */ { public: virtual bool visit( const rock & ) const = 0; virtual bool visit( const paper & ) const = 0; virtual bool visit( const scissors & ) const = 0; virtual bool fight( const gesture & ) const = 0; }; class rock : public gesture /* C */ { bool visit( const rock & ) const override { return false; } bool visit( const paper & ) const override { return true; } bool visit( const scissors & ) const override { return false; } bool fight( const gesture &g ) const override /* C */ { return g.visit( *this ); } }; class paper : public gesture /* C */ { bool visit( const rock & ) const override { return false; } bool visit( const paper & ) const override { return false; } bool visit( const scissors & ) const override { return true; } bool fight( const gesture &g ) const override /* C */ { return g.visit( *this ); } }; class scissors : public gesture /* C */ { bool visit( const rock & ) const override { return true; } bool visit( const paper & ) const override { return false; } bool visit( const scissors & ) const override { return false; } bool fight( const gesture &g ) const override /* C */ { return g.visit( *this ); } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹08.r1_bom›] #define UNIT r1_bom #include #include #include #include The base class. It remembers the part number and provides the required interface: ‹description› and ‹part_no›. Do not forget the ‹virtual› destructor! class part /* C */ { std::string _part_no; public: part( std::string pn ) : _part_no( pn ) {} virtual std::string description() const = 0; std::string part_no() const { return _part_no; } virtual ~part() = default; }; The two derived classes, 80 % boilerplate. class resistor : public part /* C */ { int _resistance; public: resistor( std::string pn, int r ) : part( pn ), _resistance( r ) {} std::string description() const override /* C */ { return std::string( "resistor " ) + std::to_string( _resistance ) + "Ω"; } }; class capacitor : public part /* C */ { int _capacitance; public: capacitor( std::string pn, int c ) : part( pn ), _capacitance( c ) {} std::string description() const override /* C */ { return std::string( "capacitor " ) + std::to_string( _capacitance ) + "μF"; } }; The smart pointer to hold and own instances of ‹part›. using part_ptr = std::unique_ptr< part >; /* C */ The ‹bom› class itself holds the parts using the above pointer. It would be possible to use ‹std::map› too (and also more efficient for longer part lists). Here, we use an ‹std::vector› of pairs, where the pair holds the part pointer and the quantity. When the item with the given order number is not on the list, we throw an exception. class bom /* C */ { using item = std::pair< part_ptr, int >; std::vector< item > _parts; Find the item in the list: the common parts of ‹find› and ‹qty›. const item &_find( std::string pn ) const /* C */ { for ( const auto &part : _parts ) if ( part.first->part_no() == pn ) return part; throw std::runtime_error( "part not found" ); } public: /* C */ We don't bother with duplicates. Notice the ‹std::move› though -- we have to transfer the ownership of the ‹part› instance to the vector (via the pair). void add( part_ptr p, int c ) /* C */ { _parts.emplace_back( std::move( p ), c ); } const part &find( std::string pn ) const /* C */ { return *_find( pn ).first; } int qty( std::string pn ) const { return _find( pn ).second; } /* C */ }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹08.r2_circuit›] #define UNIT r2_circuit The base class. We keep track of the inputs using raw pointers, since we do not own them. We use a ‹protected virtual› method to implement the ‘business logic’ that changes from class to class, while the outside interface is defined entirely using standard (non-virtual) methods. class component /* C */ { component *left = nullptr, *right = nullptr; protected: /* C */ virtual bool eval( bool, bool ) = 0; public: /* C */ void connect( int n, component &c ) { ( n ? right : left ) = &c; } bool read() /* C */ { return eval( left ? left->read() : false, right ? right->read() : false ); } virtual ~component() = default; /* C */ }; The NAND gate and the ‹source› component are trivial enough. class nand : public component /* C */ { bool eval( bool x, bool y ) override { return !( x && y ); } }; class source : public component /* C */ { bool eval( bool, bool ) override { return true; } }; The ‹delay› component provides one bit of memory. Reading the component will cause the value to be updated (‹read› always calls ‹eval› internally). This class is also the reason why ‹eval› cannot be marked ‹const›. class delay : public component /* C */ { bool _value = false; bool eval( bool x, bool ) override { bool rv = _value; _value = x; return rv; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹08.r3_loops›] #define UNIT r3_loops class component /* C */ { int left_i, right_i; component *left = nullptr, *right = nullptr; protected: /* C */ virtual bool eval_0( bool, bool ) { return false; } virtual bool eval_1( bool, bool ) { return false; } bool get_left() const { return left ? left->read( left_i ) : false; } /* C */ bool get_right() const { return right ? right->read( right_i ) : false; } public: /* C */ void connect( int i, int o, component &c ) { ( i ? right_i : left_i ) = o; ( i ? right : left ) = &c; } virtual bool read( int o ) /* C */ { auto l = get_left(); auto r = get_right(); if ( o == 0) return eval_0( l, r ); else return eval_1( l, r ); } virtual ~component() = default; /* C */ }; class cnot : public component /* C */ { bool eval_0( bool x, bool ) override { return x; } bool eval_1( bool x, bool y ) override { if ( x ) return y; else return !y; } }; class nand : public component /* C */ { bool eval_0( bool x, bool y ) override { return !( x && y ); } bool eval_1( bool x, bool y ) override { return x && y; } }; class eq : public component /* C */ { bool eval_0( bool x, bool y ) override { return x == y; } bool eval_1( bool x, bool y ) override { return x != y; } }; class delay : public component /* C */ { bool _x = false, _y = false; bool _in_read = false; bool read( int o ) override /* C */ { bool out = o ? _y : _x; if ( _in_read ) /* C */ return out; _in_read =true; /* C */ _x = get_left(); _y = get_right(); _in_read = false; return out; /* C */ } }; class latch : public component /* C */ { bool _value = false; bool eval_0( bool x, bool y ) override { return eval( x, y ); } /* C */ bool eval_1( bool x, bool y ) override { return !eval( x, y ); } bool eval( bool x, bool y ) /* C */ { if ( !x && y ) _value = true; if ( x ) _value = false; return _value; /* C */ } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹08.r4_pretty›] #define UNIT r4_pretty class node; /* C */ using node_ptr = std::unique_ptr< node >; class node /* C */ { public: virtual int precedence() const = 0; virtual void print( std::stringstream &out, bool enclose ) const = 0; std::string print() const /* C */ { std::stringstream out; print( out, false ); return out.str(); } virtual ~node() = default; /* C */ }; class constant : public node /* C */ { int _value; public: constant( int v ) : _value( v ) {} int precedence() const override { return 100; } void print( std::stringstream &o, bool enclose ) const override /* C */ { assert( !enclose ); o << _value; } }; class binary : public node /* C */ { node_ptr _left, _right; public: binary( node_ptr l, node_ptr r ) : _left( std::move( l ) ), _right( std::move( r ) ) {} virtual char op_char() const = 0; void print( std::stringstream &o, bool enclose ) const override /* C */ { if ( enclose ) o << "( "; _left->print( o, _left->precedence() < precedence() ); o << " " << op_char() << " "; _right->print( o, _right->precedence() < precedence() ); if ( enclose ) o << " )"; } }; class equality : public binary /* C */ { public: using binary::binary; int precedence() const override { return 1; } char op_char() const override { return '='; } }; class addition : public binary /* C */ { public: using binary::binary; int precedence() const override { return 2; } char op_char() const override { return '+'; } }; class multiplication : public binary /* C */ { public: using binary::binary; int precedence() const override { return 3; } char op_char() const override { return '*'; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹08.r5_json›] #define UNIT r5_json class node /* C */ { public: std::string print() const { std::stringstream str; format( str ); return str.str(); } virtual void format( std::stringstream &out ) const = 0; /* C */ virtual ~node() = default; }; using node_ptr = std::unique_ptr< node >; /* C */ class number : public node /* C */ { int _value; public: explicit number( int v ) : _value( v ) {} void format( std::stringstream &out ) const override /* C */ { out << _value; } }; auto _print_list = []( const auto &items, auto format, /* C */ std::stringstream &out, char open, char close ) { bool first = true; for ( const auto &v : items ) /* C */ { out << ( first ? open : ',' ) << " "; format( v ); first = false; } out << ( first ? open : ' ' ) << close; /* C */ }; class object : public node /* C */ { std::vector< std::pair< std::string, node_ptr > > _items; public: void format( std::stringstream &out ) const override { auto format = [&]( const auto &item ) { const auto &[ key, value ] = item; out << '"' << key << "\": "; value->format( out ); }; _print_list( _items, format, out, '{', '}' ); /* C */ } void append( std::string s, node_ptr v ) /* C */ { _items.emplace_back( s, std::move( v ) ); } }; class array : public node /* C */ { std::vector< node_ptr > _items; public: void format( std::stringstream &out ) const override { auto format = [&]( const auto &v ) { v->format( out ); }; _print_list( _items, format, out, '[', ']' ); } void append( node_ptr v ) /* C */ { _items.emplace_back( std::move( v ) ); } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹08.r6_while›] class statement; /* C */ using state = std::map< std::string, int >; using stmt_ptr = std::unique_ptr< statement >; class statement /* C */ { public: virtual state eval( state s ) const = 0; virtual ~statement() = default; }; class stmt_inc : public statement /* C */ { std::string _var; public: stmt_inc( std::string v ) : _var( v ) {} state eval( state s ) const override /* C */ { s[ _var ] ++; return s; } }; class stmt_while : public statement /* C */ { std::string _v_1, _v_2; stmt_ptr _body; public: /* C */ stmt_while( std::string v1, std::string v2, stmt_ptr b ) : _v_1( v1 ), _v_2( v2 ), _body( std::move( b ) ) {} state eval( state s ) const override /* C */ { while ( s[ _v_1 ] != s[ _v_2 ] ) s = _body->eval( std::move( s ) ); return s; } }; class stmt_block : public statement /* C */ { std::vector< stmt_ptr > _body; public: /* C */ void append( stmt_ptr stmt ) { _body.emplace_back( std::move( stmt ) ); } state eval( state s ) const override /* C */ { for ( const auto &stmt : _body ) s = stmt->eval( std::move( s ) ); return s; } }; #define UNIT r6_while /* C */ #include "test_main.cpp" ## 9. Week 9 ### [‹09.e1_iota›] #define UNIT e1_iota template< typename fun_t > /* C */ void iota( fun_t f, int start, int end ) { for ( int i = start; i < end; ++ i ) f( i ); } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹09.e2_quot›] #define UNIT e2_quot template< typename id_t > /* id for integral domain */ /* C */ class rat { id_t p, q; public: rat( id_t p, id_t q ) : p( p ), q( q ) {} bool operator==( rat r ) const { return p * r.q == r.p * q; } /* C */ friend rat operator+( rat a, rat b ) /* C */ { return { a.p * b.q + b.p * a.q, a.q * b.q }; } rat operator*( rat r ) const { return { p * r.p, q * r.q }; } /* C */ rat operator/( rat r ) const { return { p * r.q, q * r.p }; } }; class gauss /* C */ { int r, i; public: gauss( int r, int i ) : r( r ), i( i ) {} gauss operator+( gauss b ) const /* C */ { return { r + b.r, i + b.i }; } gauss operator*( gauss b ) const /* C */ { return { r * b.r - i * b.i, r * b.i + i * b.r }; } bool operator==( gauss b ) const /* C */ { return r == b.r && i == b.i; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹09.e3_split›] #define UNIT e3_split #include "test_main.cpp" split_view split( std::string_view s, char delim ) /* C */ { size_t idx = s.find( delim ); if ( idx == s.npos ) return { s, "" }; else return { s.substr( 0, idx ), s.substr( idx + 1, s.npos ) }; } ### [‹09.r1_tfold›] #define UNIT r1_tfold template< typename value_t > struct tree; template< typename fun_t, typename value_t > /* C */ value_t tfold( fun_t f, const tree< value_t > &t ); #include "test_main.cpp" /* C */ template< typename fun_t, typename value_t > /* C */ value_t tfold( fun_t f, const tree< value_t > &t ) { if ( !t.left ) return t.value; auto left = tfold( f, *t.left ), /* C */ right = tfold( f, *t.right ); return f( f( t.value, left ), right ); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹09.r2_tmap›] #define UNIT r2_tmap #include #include template< typename value_t > /* C */ struct tree; template< typename fun_t, typename val_t > /* C */ using mapped_tree = tree< std::invoke_result_t< fun_t, val_t > >; template< typename fun_t, typename val_t > /* C */ using mapped_vec = std::vector< std::invoke_result_t< fun_t, val_t > >; template< typename fun_t, typename value_t > /* C */ mapped_tree< fun_t, value_t > tmap( fun_t f, const tree< value_t > &t ); #include "test_main.cpp" /* C */ template< typename fun_t, typename value_t > /* C */ auto map( fun_t f, const std::vector< value_t > &vec ) { mapped_vec< fun_t, value_t > out; for ( const auto &v : vec ) out.push_back( f( v ) ); return out; } template< typename fun_t, typename value_t > /* C */ mapped_tree< fun_t, value_t > tmap( fun_t f, const tree< value_t > &t ) { using mt = mapped_tree< fun_t, value_t >; auto map_sub = [&]( const auto &subtree ) /* C */ { return tmap( f, subtree ); }; return mt( f( t.value ), map( map_sub, t.children ) ); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹09.r3_monoid›] #define UNIT r3_monoid #include template< typename hom_t > /* C */ struct elem { std::string v; hom_t h; elem( std::string s, hom_t h ) : v( s ), h( h ) {} /* C */ elem operator*( const elem &e ) const { return { v + e.v, h }; } bool operator==( const elem &e ) const { return h( v ) == h( e.v ); } }; template< typename hom_t > /* C */ class monoid { hom_t h; public: monoid( hom_t h ) : h( h ) {} ::elem< hom_t > elem( std::string s ) { return { s, h }; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹09.r4_treap›] #define UNIT r4_treap template< typename key_t > /* C */ class treap { class node { key_t _key; int _priority; std::unique_ptr< node > _left, _right; public: /* C */ node( key_t k, int p ) : _key( std::move( k ) ), _priority( p ) {} int priority() const { return _priority; } /* C */ const key_t &key() const { return _key; } auto &child( bool dir ) { return dir ? _left : _right; } /* C */ auto left() const { return _left.get(); } auto right() const { return _right.get(); } }; using node_ptr = std::unique_ptr< node >; /* C */ static node_ptr rotate( node_ptr self, bool dir ) /* C */ { auto root = std::move( self->child( dir ) ); auto detach = std::move( root->child( !dir ) ); assert( root ); /* C */ self->child( dir ) = std::move( detach ); root->child( !dir ) = std::move( self ); return root; } static node_ptr fix( node_ptr self, bool dir ) /* C */ { assert( self->child( dir ) ); if ( self->priority() > self->child( dir )->priority() ) /* C */ return self; else return rotate( std::move( self ), dir ); } static node_ptr insert( node_ptr self, key_t key, int prio ) /* C */ { if ( !self ) return std::make_unique< node >( std::move( key ), prio ); bool dir = key < self->key(); /* C */ auto &child = self->child( dir ); child = insert( std::move( child ), std::move( key ), prio ); return fix( std::move( self ), dir ); } node_ptr _root; /* C */ public: /* C */ void insert( key_t key, int prio ) { _root = insert( std::move( _root ), std::move( key ), prio ); } const node *root() const { return _root.get(); } /* C */ }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹09.r6_finally›] #define UNIT r6_finally template< typename handler_t > /* C */ class finally { std::optional< handler_t > _handler; public: explicit finally( handler_t h ) : _handler( std::move( h ) ) {} finally( const finally & ) = delete; /* C */ finally &operator=( const finally & ) = delete; finally( finally &&other ) /* C */ : _handler( std::move( other._handler ) ) { other.cancel(); } finally &operator=( finally && other ) /* C */ { if ( other._handler ) _handler.emplace( std::move( *other._handler ) ); other.cancel(); return *this; } void cancel() { _handler.reset(); } /* C */ ~finally() /* C */ { if ( _handler ) ( *_handler )(); } }; #include "test_main.cpp" /* C */ ## 10. Week 10 ### [‹10.e1_format›] #define UNIT e1_format #include #include #include template< typename T > /* C */ std::string format( const T &coll, char b, char e ) { int i = 0; std::ostringstream str; for ( const auto &e : coll ) str << ( i++ ? ',' : b ) << " " << e; if ( i ) str << " " << e; else str << b << e; return str.str(); } template< typename T > /* C */ std::string format( const std::vector< T > &s ) { return format( s, '[', ']' ); } template< typename T > /* C */ std::string format( const std::set< T > &s ) { return format( s, '{', '}' ); } #include "test_main.cpp" /* C */ ### [‹10.e2_concat›] #define UNIT e2_concat #include template< typename seq1_t, typename seq2_t > /* C */ auto concat( const seq1_t &s1, const seq2_t &s2 ) { std::vector< typename seq1_t::value_type > out; for ( const auto &x : s1 ) /* C */ out.push_back( x ); for ( const auto &x : s2 ) out.push_back( x ); return out; /* C */ } #include "test_main.cpp" /* C */ ### [‹10.e3_select›] #define UNIT e3_select #include #include template< typename seq1_t, typename seq2_t > /* C */ auto select( const seq1_t &s1, const seq2_t &s2, const std::vector< bool > &bmp ) { using variant = std::variant< typename seq1_t::value_type, typename seq2_t::value_type >; std::vector< variant > out; auto i = s1.begin(); /* C */ auto j = s2.begin(); for ( bool first : bmp ) /* C */ { out.emplace_back( first ? variant( *i ) : variant( *j ) ); ++ i, ++ j; } return out; /* C */ } #include "test_main.cpp" /* C */ ### [‹10.r1_icons›] #define UNIT r1_icons struct null; template< typename cdr_t > /* C */ struct cons { int car; cdr_t cdr; cons( int car, const cdr_t &cdr ) : car( car ), cdr( cdr ) {} }; int sum( null ); /* C */ template< typename cons_t > /* C */ int sum( const cons_t & c ) { return c.car + sum( c.cdr ); } #include "test_main.cpp" /* C */ int sum( null ) /* C */ { return 0; } ### [‹10.r2_sorted›] #define UNIT r2_sorted #include struct check_sorted /* C */ { std::any last; bool mismatch = false; bool was_sorted() const { return !mismatch; } /* C */ template< typename value_t > /* C */ void operator()( const value_t &v ) { if ( last.has_value() ) { if ( std::any_cast< value_t >( last ) > v ) mismatch = true; } last = v; /* C */ } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹10.r3_fsm›] #define UNIT r3_fsm #include #include template< typename letter_t > /* C */ class fsm { std::map< letter_t, const fsm * > _next; bool _accept; public: /* C */ explicit fsm( bool a = false ) : _accept( a ) {} void next( letter_t c, const fsm &n ) { _next[ c ] = &n; } template< typename seq_t > /* C */ bool accept( const seq_t &s ) const { return accept( s.begin(), s.end() ); } template< typename iter_t > /* C */ bool accept( iter_t b, iter_t e ) const { if ( b == e ) return _accept; if ( auto n = _next.find( *b ); n != _next.end() ) /* C */ return n->second->accept( ++b, e ); else return false; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹10.r5_bimap›] #define UNIT r5_bimap template< typename left_t, typename right_t > /* C */ class bimap { std::map< left_t, const right_t * > _right; std::map< right_t, const left_t * > _left; public: bool insert( left_t l, right_t r ) /* C */ { if ( _left.count( r ) || _right.count( l ) ) return false; auto [ ri, ri_n ] = _right.emplace( std::move( l ), nullptr ); /* C */ auto [ li, li_n ] = _left.emplace( std::move( r ), nullptr ); ri->second = &li->first; /* C */ li->second = &ri->first; return true; /* C */ } template< typename map_t, typename key_t > /* C */ static auto get( const map_t &map, const key_t &key ) { auto it = map.find( key ); return it == map.end() ? nullptr : it->second; } const left_t *get_left( const right_t &r ) const /* C */ { return get( _left, r ); } const right_t *get_right( const left_t &l ) const /* C */ { return get( _right, l ); } void erase( const left_t &l, const right_t &r ) /* C */ { _left.erase( r ); _right.erase( l ); } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹10.r6_tinyvec›] #define UNIT r6_tinyvec #include #include #include class insufficient_space; /* C */ void throw_insufficient_space(); template< typename value_t, size_t max_size > /* C */ class tiny_vector { std::array< uint8_t, max_size * sizeof( value_t ) > _mem; int _count = 0; public: /* C */ value_t *slot( int i ) { assert( i >= 0 ); assert( i < _count ); return reinterpret_cast< value_t * >( _mem.begin() ) + i; } const value_t *slot( int i ) const /* C */ { return reinterpret_cast< const value_t * >( _mem.begin() ) + i; } ~tiny_vector() /* C */ { while ( _count ) erase( _count - 1 ); } void erase( int idx ) /* C */ { std::destroy_at( slot( idx ) ); for ( int i = idx; i < _count - 1; ++i ) /* C */ { std::uninitialized_move_n( slot( i + 1 ), 1, slot( i ) ); std::destroy_at( slot( i + 1 ) ); } -- _count; /* C */ } void insert( int idx, value_t &&v ) /* C */ { if ( _count == max_size ) throw_insufficient_space(); ++ _count; /* C */ for ( int i = _count - 1; i > idx; --i ) /* C */ { std::uninitialized_move_n( slot( i - 1 ), 1, slot( i ) ); std::destroy_at( slot( i - 1 ) ); } if ( idx < _count - 1 ) /* C */ std::destroy_at( slot( idx ) ); std::uninitialized_move_n( &v, 1, slot( idx ) ); /* C */ } const value_t &front() const { return *slot( 0 ); } /* C */ const value_t &back() const { return *slot( _count - 1 ); } value_t &front() { return *slot( 0 ); } /* C */ value_t &back() { return *slot( _count - 1 ); } }; #include "test_main.cpp" /* C */ void throw_insufficient_space() /* C */ { throw insufficient_space(); } ## 11. Week 11 ### [‹11.e1_iota›] #define UNIT e1_iota struct iota_iterator /* C */ { using iterator = iota_iterator; int _val; bool operator==( iterator o ) const { return _val == o._val; }; bool operator!=( iterator o ) const { return _val != o._val; }; iota_iterator &operator++() { ++ _val; return *this; } int operator*() const { return _val; } }; class iota /* C */ { int _start, _end; public: iota_iterator begin() const { return { _start }; } iota_iterator end() const { return { _end }; } iota( int s, int e ) : _start( s ), _end( e ) {} }; #include "test_main.cpp" /* C */ ### [‹11.e2_view›] #define UNIT e2_view template< typename iter_t > /* C */ class view { iter_t _begin, _end; public: view( iter_t b, iter_t e ) : _begin( b ), _end( e ) {} iter_t begin() const { return _begin; } iter_t end() const { return _end; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹11.e3_skip›] #define UNIT e3_skip template< typename iter_t > /* C */ class skip { iter_t _begin, _end; int _skip; public: struct iterator /* C */ { iter_t iter, end; int skip; decltype( auto ) operator*() { return *iter; } /* C */ decltype( auto ) operator*() const { return *iter; } bool operator==( iterator o ) const /* C */ { return iter == o.iter; } bool operator!=( iterator o ) const /* C */ { return iter != o.iter; } iterator operator++( int ) /* C */ { iterator i = *this; ++*this; return i; } iterator &operator++() /* C */ { for ( int i = 0; i < skip; ++ i ) if ( iter != end ) ++iter; return *this; } }; skip( iter_t b, iter_t e, int s ) /* C */ : _begin( b ), _end( e ), _skip( s ) {} iterator begin() const { return { _begin, _end, _skip }; } /* C */ iterator end() const { return { _end, _end, _skip }; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹11.r1_map›] #define UNIT r1_map We first define the iterator. It is convenient to take the «underlying iterator» as a type parameter (instead of the container), though the latter would also work. The other type parameter is the lambda to call on each dereference. template< typename iterator_t, typename fun_t > /* C */ struct map_iterator { iterator_t it; const fun_t &fun; Construct an iterator. The signature makes template argument deduction work, which we will use to our advantage below. map_iterator( iterator_t it, const fun_t &fun ) /* C */ : it( it ), fun( fun ) {} The dereference operator first dereferences the underlying iterator, applies ‹fun› to it and returns the result. The return type of the dereference operator is tricky, so we let the compiler figure it out for us. auto operator*() const { return fun( *it ); } /* C */ Pre-increment simply calls the underlying pre-increment. map_iterator &operator++() { ++it; return *this; } /* C */ Same thing with inequality. bool operator!=( const map_iterator &o ) const /* C */ { return it != o.it; } }; The ‹map› class template. Here we take the underlying «container» and the type of the lambda as parameters, since those are what the user will supply as arguments to the constructor. This way, template argument deduction will work for users as expected. template< typename container_t, typename fun_t > /* C */ struct map { There are two ways to go about building the iterator type. One is explicitly, by figuring out the type of the underlying iterator (i.e. the iterator of ‹container_t› and creating an explicit instance of ‹map_iterator›. We will use this in ‹begin›. using underlying = typename container_t::const_iterator; /* C */ using iterator = map_iterator< underlying, fun_t >; const container_t &container; /* C */ const fun_t &fun; The ‹begin› method needs to construct a suitable ‹map_iterator›: we built the correct type above, so we can use that as the return type of ‹begin›, then use ‹return› with braces to call the constructor. iterator begin() const { return { container.begin(), fun }; } /* C */ An alternative, which does not need to mention the type of the underlying iterator, but instead relies on the argument deduction that we were careful to build into the constructor of ‹map_iterator›. auto end() const /* C */ { return map_iterator( container.end(), fun ); } Finally the constructor of ‹map› which lets us conveniently create instances through template argument deduction. map( const container_t &c, const fun_t &f ) /* C */ : container( c ), fun( f ) {} }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹11.r2_range›] #define UNIT r2_range #include #include template< typename container_t > /* C */ class range { using data_ptr = std::shared_ptr< container_t >; using iterator = typename container_t::const_iterator; data_ptr _data; iterator _b, _e; public: /* C */ range( container_t c ) : _data( std::make_shared< container_t >( std::move( c ) ) ), _b( _data->begin() ), _e( _data->end() ) {} range( data_ptr c, iterator b, iterator e ) /* C */ : _data( c ), _b( b ), _e( e ) {} auto begin() const { return _b; } /* C */ auto end() const { return _e; } range take( int n ) const /* C */ { return { _data, _b, std::next( _b, n ) }; } range drop( int n ) const /* C */ { return { _data, std::next( _b, n ), _e }; } template< typename C > /* C */ friend bool operator==( range a, range< C > b ) { return std::equal( a.begin(), a.end(), b.begin(), b.end() ); } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹11.r3_permute›] #define UNIT r3_permute #include #include struct permutations /* C */ { struct permutation { std::vector< int > p; auto begin() const { return p.begin(); } auto end() const { return p.end(); } int operator[]( int i ) const { return p[ i ]; } }; struct iterator /* C */ { std::vector< int > p; iterator &operator++() /* C */ { if ( !std::next_permutation( p.begin(), p.end() ) ) p.clear(); return *this; } permutation operator*() const { return { p }; } /* C */ bool operator!=( const iterator &o ) const /* C */ { return p != o.p; } bool operator==( const iterator &o ) const /* C */ { return p == o.p; } }; std::vector< int > first; /* C */ permutations( std::vector< int > v ) : first( std::move( v ) ) /* C */ { std::sort( first.begin(), first.end() ); } iterator begin() const { return { first }; } /* C */ iterator end() const { return {}; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹11.r5_matrix›] #define UNIT r5_matrix template< typename scalar_t, size_t ncols, size_t nrows > /* C */ class matrix { using row = std::array< scalar_t, ncols >; std::array< row, nrows > _data; template< typename matrix_t, typename value_t > /* C */ struct column_iterator { using value_type = value_t; using reference = value_t &; using pointer = value_t *; using difference_type = ssize_t; using iterator_category = std::forward_iterator_tag; matrix_t *_matrix; /* C */ int _col; int _row; column_iterator &operator++() /* C */ { ++ _row; return *this; } pointer operator->() const { return &**this; } /* C */ reference operator*() const { return _matrix->_data[ _row ][ _col ]; } bool operator==( column_iterator o ) const /* C */ { return _col == o._col && _row == o._row; } bool operator!=( column_iterator o ) const /* C */ { return !( *this == o ); } }; template< typename matrix_t, typename value_t > /* C */ struct column { using iterator = column_iterator< matrix_t, value_t >; matrix_t *_matrix; int _col; iterator begin() const { return { _matrix, _col, 0 }; } /* C */ iterator end() const { return { _matrix, _col, nrows }; } }; template< typename value_t > /* C */ struct proxy { value_t _value; value_t *operator->() { return &_value; } }; template< typename matrix_t, typename value_t > /* C */ struct columns_iterator { using value_type = column< matrix_t, value_t >; using reference = value_type; using pointer = value_type *; using difference_type = ssize_t; using iterator_category = std::input_iterator_tag; matrix_t *_matrix; /* C */ int _col; columns_iterator &operator++() /* C */ { ++ _col; return *this; } proxy< value_type > operator->() const { return { **this }; } /* C */ reference operator*() const { return { _matrix, _col }; } bool operator==( columns_iterator o ) const /* C */ { return o._matrix == _matrix && o._col == _col; } bool operator!=( columns_iterator o ) const /* C */ { return !( *this == o ); } }; template< typename matrix_t, typename value_t > /* C */ struct columns { using iterator = columns_iterator< matrix_t, value_t >; matrix_t *_matrix; iterator begin() const { return { _matrix, 0 }; } iterator end() const { return { _matrix, ncols }; } }; public: /* C */ matrix( std::initializer_list< scalar_t > il ) /* C */ { auto it = data( il ); for ( size_t i = 0; i < ncols * nrows; ++i ) /* C */ { assert( it != il.end() ); _data[ i / ncols ][ i % ncols ] = *it++; } assert( it == il.end() ); /* C */ } auto &rows() const { return _data; } /* C */ columns< const matrix, const scalar_t > cols() const { return { this }; } columns< matrix, scalar_t > cols() { return { this }; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹11.r6_bits›] #define UNIT r6_bits #include template< typename num_t > /* C */ class bits { struct proxy { bool _value; bool *operator->() { return &_value; } }; struct iterator /* C */ { using value_type = bool; using reference = bool; using pointer = proxy; using difference_type = ssize_t; using iterator_category = std::input_iterator_tag; num_t _value; /* C */ num_t _mask; iterator &operator++() /* C */ { _mask <<= 1; return *this; } bool operator*() const { return _value & _mask; } /* C */ proxy operator->() const { return { **this }; } bool operator==( const iterator &other ) const /* C */ { return _mask == other._mask; } bool operator!=( const iterator &other ) const /* C */ { return !( *this == other ); } }; num_t _value; /* C */ public: /* C */ bits( num_t n ) : _value( n ) {} iterator begin() const { return { _value, 1 }; } /* C */ iterator end() const { return { _value, 0 }; } }; #include "test_main.cpp" /* C */ ## 12. Week 12 ### [‹12.e1_digraph›] #define UNIT e1_digraph #include #include struct strmap /* C */ { std::map< std::string, int > m; int operator[]( std::string s ) const /* C */ { return m.count( s ) ? m.find( s )->second : 0; } void add( std::string s ) /* C */ { m[ s ] ++; } }; strmap digraph_freq( const std::string &s ) /* C */ { strmap m; for ( size_t i = 0; i < s.size() - 1; ++i ) /* C */ if ( std::isalpha( s[ i ] ) && std::isalpha( s[ i + 1 ] ) ) m.add( s.substr( i, 2 ) ); return m; /* C */ } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹12.e2_spelling›] #define UNIT e2_spelling #include #include #include class spell /* C */ { std::set< std::string > _words; public: spell( const char *fn ) /* C */ { std::ifstream words( fn ); std::string word; while ( std::getline( words, word ) ) _words.insert( word ); } bool check( const std::string s ) const /* C */ { return _words.count( s ); } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹12.e3_ternary›] #define UNIT e3_ternary struct tristate /* C */ { bool val, det; }; const tristate yes { true, true }; /* C */ const tristate no { false, true }; const tristate maybe{ false, false }; tristate operator&&( tristate a, tristate b ) /* C */ { if ( a.det && b.det ) return a.val && b.val ? yes : no; if ( ( a.det && !a.val ) || ( b.det && !b.val ) ) return no; else return maybe; } tristate operator||( tristate a, tristate b ) /* C */ { if ( a.det && b.det ) return a.val || b.val ? yes : no; if ( ( a.det && a.val ) || ( b.det && b.val ) ) return yes; else return maybe; } bool operator==( tristate a, tristate b ) /* C */ { if ( a.det && b.det ) return a.val == b.val; else return a.det == b.det; } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹12.r1_trie›] #define UNIT r1_trie #include #include #include using key = std::vector< bool >; /* C */ struct node /* C */ { std::shared_ptr< node > l, r; std::weak_ptr< node > up; node( std::shared_ptr< node > u ) : up( u ) {} bool val() const /* C */ { assert( up.lock()->l.get() == this || up.lock()->r.get() == this ); return up.lock()->r.get() == this; } ::key key() const /* C */ { ::key k; if ( up.lock() ) { k = up.lock()->key(); k.push_back( val() ); } return k; } }; class trie /* C */ { using ptr = std::shared_ptr< node >; ptr r; using ref = node &; public: /* C */ trie() : r( std::make_shared< node >( nullptr ) ) {} auto make( ptr u ) { return std::make_shared< node >( u ); } /* C */ ptr add( ptr n, bool l ) /* C */ { return ( l ? n->r : n->l ) = make( n ); } ptr add_amb( ptr n ) /* C */ { return n->r = n->l = make( n ); } ptr find( key k, int idx, ptr n ) const /* C */ { if ( idx == int( k.size() ) ) return n; return find( k, idx + 1, k[ idx ] ? n->r : n->l ); } ptr find( key k ) const { return find( k, 0, r ); } /* C */ ptr root() const { return r; } }; #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹12.r2_cooking›] #define UNIT r2_cooking #include #include class pantry /* C */ { public: std::map< std::string, int > stuff; int count( std::string s ) const { return stuff.at( s ); } void add( std::string s, int v ) { stuff[ s ] += v; } }; class recipe /* C */ { public: std::map< std::string, std::pair< int, int > > stuff; void add( std::string s, int v, int o = 0 ) /* C */ { stuff[ s ].first += v; stuff[ s ].second += o; } }; bool cook( pantry &p, const recipe &r, int qty ) /* C */ { for ( const auto &[ k, v ] : r.stuff ) if ( qty * v.first > p.count( k ) ) return false; for ( const auto &[ k, v ] : r.stuff ) /* C */ { p.stuff[ k ] -= qty * v.first; if ( p.count( k ) > qty * v.second ) p.stuff[ k ] -= qty * v.second; } return true; /* C */ } #include "test_main.cpp" /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### [‹12.r3_cards›] #define UNIT r3_cards #include class card /* C */ { char suit, rank; public: friend std::ostream &operator<<( std::ostream &o, card c ) /* C */ { return o << c.rank << c.suit; } friend std::istream &operator>>( std::istream &i, card &c ) /* C */ { char ch; i >> ch; if ( ( !std::isdigit( ch ) && /* C */ ch != 'A' && ch != 'J' && ch != 'Q' && ch != 'K' && ch != 'T' ) || ( i.peek() != 'D' && i.peek() != 'S' && i.peek() != 'H' && i.peek() != 'C' ) || ch == '0' ) { i.unget(); i.setstate( i.failbit ); return i; } c.rank = ch; /* C */ return i >> c.suit; } }; #include "test_main.cpp" /* C */